Rails and Ajax for Page Application Development (ETech 2006 Tutorial)


I'm in David Heinemeier Hansson's tutorial on Beneath-the-Page Application Development with Rails. His Rails tutorial from last summer remains one of my most viewed blog entries.

He starts out noting that AJAX is the most important innovation for the Web in years.

But JavaScripting the DOM still sucks...a lot. JavaScripting the DOM is incompatible with how regular programmers think about programming.

Part of the problem is the sorry state of browser. One line of change can lead to hours of regressions because of browser incompatibilities. Then there's the browser underworld (all the old, out of date browsers that are still out there). That's the bad.

Then there's the ugly. Nodes are not for people. The idea of "createNode" as an API call has a nice academic feel, but it doesn't add up to something that's pleasant for developers to use. But the main problem with nodes is that they cause you to repeat yourself. You have to create the first version of the page using HTML. Your entire UI is mapped out in template files. To make a dynamic change, you have to say it again another way. This creates two versions of the interface--you have to take great care to ensure that they don't drift apart.

Now, for the good: innerHTML is the hero. A triumph of pragmatism. No spec, but it's supported by all. innerHTML's companion, the saint, is eval. You can construct JavaScript outside the browser, that is you can generate it, and then evaluate the result. Compare this with what Simon Willison said this morning for a contrasting point of view.

So, how do we deal with the bad, not return calls to the ugly, and champion the good?

Rails uses the Prototype JavaScript library to try to put a layer on top of the browser incompatibilities. You write off the underworld as being too expensive and concentrate on IE, Firefox, and Safari. On top of Prototype, Rails uses script.aculo.us. All of these elements (rails, prototype, and script.aculo.us) are written with explicit knowledge of each other.

David is building a demo application from scratch using Rails (v1.1) to show his approach to page development and AJAX. The first time I saw this it was amazing. I did one for Kelly Flanagan (BYU CIO) a few months ago, so if I can do it...maybe it's not so amazing! :-) You can get the functionality he's demoing today using beta Gems.

The first part of the demo follows the basics "use rails to create a MVC system" script pretty closely. He's building a simple ecommerce site. Once he's got a way to add products, display them, and adding things to a shopping cart, he starts adding AJAX.

Transforming the rails link_to to link_to_remote creates an AJAX request to the same URL. The default action is to eval the links coming back. The other change is to remove the default rendering and replace it with a call to an rjs template. The rjs template returns JavaScript that's eval'd in the browser. In rjs, there's a proxy for the DOM in Ruby called page. Here's what's in the rjs file:

page[:cart].replace_html :partial=> "cart"

From within the page, you can get a reference to specific parts of the page and replace them with something else. This makes use of "partials" an rhtml page with a name that starts with the underscore.

So, David's pulled out the shopping cart HTML and made it a partial. That's referenced in the template and in the Ruby code. With that, he can update the shopping cart in an AJAX way without refreshing the page.

The cart has been in the session as an array. David creates an object for the cart (not persistent) and adds totals. By just changing the partial, the total is added to the shopping cart without modifying nodes in one place and HTML in another.

Adding a "discard" function to the cart demonstrates the use of inline rjs code. This is useful when the function is pretty simple. Adding this doesn't work, so he shows how you can add an "alert(exception)" to the app and get debugging info in the browser.

Adding AJAX to Web apps, changes the nature of how you think about them. To demo this, he adds a style to the cart to hide it if there's nothing in it. But when you add something to it, this doesn't go away, because the page isn't rerendered. You can add code to the rjs manage this instead of scripting the JavaScript by hand.

Next David adds a "remove from cart" link next to the product (pedagogically placed there instead of in the cart itself). He adds logic to test whether or not the product is in the cart to determine whether or not the link is shown. The problem is that you don't reload, so you need to update products whenever something is added to the cart.

This calls for another abstraction. Clipping out the product display HTML as another partial does the trick. Actually, there's two partials, one for products and one for product inside the products partial. The next problem is that the "add" method doesn't have all the products, it has the new one. That can be fixed by adding somme code. David mentions that this points out the problem of state maintenance that AJAX code introduces. I wonder if continuation-based Web applications could solve this?

We've fixed one problem, but there's a combinatorial explosion of dependencies in the UI. For example, now, when you discard the cart, the product listing needs to be updated. These things are hard enough to do when you're programming in Ruby. When you're hacking our the raw JavaScript, you often just ignore them.

How do you fall back when the browser doesn't support JavaScript? Graceful degradation is important. The nice thing about having everything in partials, is that don't have to recreate yet another view for non-compliant browsers. Adding this code to the end of the method to explicitly control the rendering is a key idiom:

if request.xhr?
   render
else
   redirect :action => "index"
end

The other key piece is to ensure that good hrefs are built in addition to the "onclick" actions. This creates some ugly code, so he defines a helper function to make it happen.

RJS isn't an attempt to replace JavaScript, but a way to generate the most common things you want to do. You can drop out and use any JavaScript you'd like when you need to.

There are two main points to what we've been talking about today. Rule 1: less JavaScript and more tolerable JavaScript. By capturing common patterns and replacing them with abstractions like replace_html we make programming in JavaScript easier. He shows how each RJS line is translated into the equivalent JavaScript.

Rule 2: Less data, more interface. This is the opposite of what AJAX has been described as. Rails isn't passing XML back and forth, rather it's passing XHTML (admittedly a form of XML). Returning a fully described partial is more bandwidth than just returning the hash, but it's worth the tradeoff. The amount of data usually isn't the problem. It's programmer time with JavaScript.

A new rule: make it snazzy. AJAX applications are distinctively different from regular Web apps. Effects not just fluff--they're there to give the user confirmation that something has happened. David shows the video from Fluxiom, an asset management application, that looks like a desktop application but is all in Rails. This is the future of Web applications.

Next David shows off a new feature of Rails that allows you to drive the application just like a browser would from the console. It's very similar to a command line browser that remembers sessions, etc. and gives them to the user as Ruby objects that can be manipulated. This is great for writing unit tests for a Web application.

David's been following microformats. Microformats, annotate XHTML in such a way that machines can easily digest it. It's a way to render data in a human readable way so that there's only one view. He returns to the shopping cart example to show how classes can be added to embed semantics. By adding "product", "name", and "price" class attributes to the template, he gets a microformat.

To make this useful for machines, he changes the controller so that the to check the content type of the request. If it is "application/xml" he assumes that the client wants just the products and not the HTML wrapper. This allows the same controller to function for both browsers and programs.

He starts writing code to process what gets returned and gets an error. Lesson: you have to generate valid XML if you're going to parse it as XML. Of course, you've got all the XHTML tags as nodes and so navigating it can be difficult (you want to navigate the class attributes). David's working on an API that would allow class attribute-based navigation.

An alternate solution is to use XSLT transforms from within Ruby to transform the XHTML to XML. This isn't actually working yet, apparently, but would be pretty easy to set up.