Wednesday, June 4, 2008

Write your javascript in ruby with rubyjs

I was inspired by Nathaniel Talbott's awesome railsconf talk to do some hacking. I had a lot of fun, and I think I came up with something worth sharing, so here it is. My willing victim was rubyjs, Michael Neumann's excellent little project I mentioned in an earlier post. If you want to play along at home, the first thing you'll need to do is fetch my code like so:
git clone git://github.com/superchris/rubyjs.git

There are some things in my repo that haven't made it into the gem version of rubyjs yet. My hack was a little "port" of the hangman example from Tapestry. It's there in examples/hangman. You can try it here. Or if you're so inclined, you can build it your dang self by running:

rake "examples/hangman/hangman.js"

This command uses a rake rule to run the rubyjs compiler which compiles hangman.rb to hangman.js.

So let's see some code how about, hmm?

class DOMElement
def initialize(element)
@dom_element = element
end

def observe(event, &block)
element = @dom_element
`
if (#<element>.addEventListener) {
#<element>.addEventListener(#<event>, #<block>, false);
} else {
#<element>.attachEvent("on" + #<event>, #<block>);
}
`
nil
end

def [](attribute)
element = @dom_element
`return #<element>[#<attribute>]`
end

def []=(attr, value)
element = @dom_element
`#<element>[#<attr>] = #<value>;`
nil
end

def self.find_js_element(element)
`return document.getElementById(#<element>);`
end


def self.find(element)
dom_element = self.find_js_element(element)
DOMElement.new(dom_element)
end

#
# Gets an HTML representation (as String) of an element's children.
#
# elem:: the element whose HTML is to be retrieved
# return:: the HTML representation of the element's children
#
def inner_html
elem = @dom_element
`
var ret = #<elem>.innerHTML;
return (ret == null) ? #<nil> : ret;`
end

#
# Sets the HTML contained within an element.
#
# elem:: the element whose inner HTML is to be set
# html:: the new html
#
def inner_html=(html)
elem = @dom_element
`
#<elem>.innerHTML = #<html>;
return #<nil>;`
end

end

The first class you see here is DOMElement. This gives some basic dom manipulation abilities in a ruby friendly way. I got this working by looking at the work Michael had done on porting GWT to ruby and extracting the bit I wanted and "rubifying" it a little. Basically, you can find elements by id, observe events with ruby blocks, and get/set attributes and inner html. Pretty simple, but has what I need. I should probably extract this into a separate file in rubyjs in case other people want it. You can also see lots of examples of how rubyjs and javascript talk to each other here.

Then it's on to the hangman code. First it's worth looking at the html so you can see what dom elements the code refers to:

And finally here's the hangman class:

class Hangman

attr_accessor :word, :letters, :misses

MAX_MISSES = 6

def initialize
@word = "snail"
@letters = @word.split ""
@guessed_letters = {}
@letters.each { |letter| @guessed_letters[letter] = false }
@misses = 0
@scaffold_div = DOMElement.find("scaffold_div")
@letters_div = DOMElement.find("letters")
@guess_input = DOMElement.find("letter")
@guess_button = DOMElement.find("guess")
@guess_button.observe("click") do
guess(@guess_input["value"])
end
@letters_div.inner_html = display_word
end

def display_word
letters.collect do |letter|
@guessed_letters[letter] ? letter : "_"
end.join
end

def guess(letter)
if letters.include?(letter)
@guessed_letters[letter] = true
@letters_div.inner_html = display_word
puts "You win!" if won?
else
@misses += 1
@scaffold_div.inner_html = "<img src='scaffold-#{@misses}.png' />"
if lost?
puts "You lost!"
@guess_button["disabled"] = true
end
end
@guess_input["value"] = ""
end

def lost?
@misses >= 6
end

def won?
@guessed_letters.values.each do |guessed|
return false unless guessed
end
return true
end

def self.main
hangman = Hangman.new
rescue StandardError => ex
puts ex
end
end

First you see the initialize method. Here is where we lookup our DOM elements, setup a Hash of which letters are guessed yet, and bind a block to the click event of our guess_button. Next comes the display_word method, which displays each letter or a blank if it's not been guessed.

The meat of the matter is in guess_letter, which is pretty simple. If the letter guessed is in the word we mark it, redisplay the word with the guessed letter and check if the user has won. If not, we update our miss count, display the right image, and check to see if the user lost. Won? and lost? are both trivial and not worth talking about.

Well, I had a lot of fun hacking on this. I think rubyjs has a good chance to be really useful as well as fun. It needs some love, certainly, but one of the things I did was start of woefully inadequate port of miniunit I'm calling microunit. It's in rubyjs/lib. This should make it easier and, I think, funner, to flesh out the core library for rubyjs.

For next steps I may expand on this example some. I'm thinking it would be a blast to tie to a Rails backend that gives me random words (right now it is always the same word :( ) or stores scores or some such foolishness. I could use this as an excuse to build an ActiveResource client for rubyjs and maybe a rubyjs on rails plugin.

If anyone cares about this, drop me a comment and let me know. Or if you think it's utterly stupid, tell me why. I promise to read all your comments and do whatever I feel like doing anyways :)

Tuesday, June 3, 2008

Back from Railsconf 2008

Wow, that was fun. Here's a quick recap of my personal highlights of Railsconf 2008.

Our Modelling Dialogue session

So I'm self-centered, but this was the talk I had the most concern about since I was in it. To be honest I had no idea how this was going to be received, so I was relieved and excited by the great response. I had this vision of all our laugh lines receiving the dead silence, crickets chirping response. But gratefully this was not the case. Jim Weirich has a great recap and lists the books we recommend on his blog. The feedback was so positive that we're already cooking up ideas for other talks in this format. To all those who came up and said something to us about our session: Thanks a ton! You have no idea what a help it is to hear from audience members about how it worked or didn't work for them. In short, there's nothing like going out on a limb and having it not break :) Major kudos to Jim and Joe for the idea for this one and for doing a great job pulling it off.

Nathaniel Talbott's hacking session


This was for me the most inspiring session. In a nutshell: hacking is good for you. If it's not fun it's not hacking. Being useful is not the point, enjoying yourself is. It inspired me to hack on rubyjs some more. I had a great time, and managed to come up with something worth sharing I think (tho as I said this was not the point, just a happy accident). More to come.

DHH Keynote

I actually enjoyed this quite a bit. Some of his points I really appreciated: he called BS on the whole IMO messed up american work culture idea that working more and sleeping less makes you supercoderdude. This is total crap, makes us less effective, and it's high time someone said so. Sleep more, and find some hobbies that don't involve computers. Get a life in other words. Great stuff.

Alternative Ruby impls

There was a lot of talk at the conference about non-MRI Ruby interpreters running Rails. I went to the IronRuby session. They run Rails and showed a simple scaffold example working. But I was most impressed by their silverlight demo of ruby running in the browser. Of course, only works in windows and maybe OSX. Allegedly moonlight will catch up. I'd love to really believe MS on that, but I'll believe it when I see it.

I didn't see the Rubinius talk but the did show it running Rails in a keynote. Boy is it slow, but if they can get performance up to snuff it sure is cool to have ruby in ruby.

Maglev IMO got a ton of unwarranted buzz. The part Avi did was cool, but the next part with the gemstone guy ruined it. They are nowhere close to running rails, and the sales guy was casting these sneaky sideways slams of other impls and selling a lot of vaporware. The perf numbers are impressive, but I totally agree with Charles that they mean nothing until they implement all of Ruby. Overall, I thought it kind of FUDDY, and I was quite saddened that the community just went along with it hook line and sinker.

JRuby was there, and had some good talks but since they've been running rails for a long while this wasn't really news and didn't get so much attention. I really like the JRuby rack stuff a lot tho, it opens even more doors. And I had at least one awesome hallway conversation about some big companies adopting JRuby and doing interesting stuff.