The author, smiling winningly Scott Raymond home

Real-world Rails RJS templates

01 Dec 2005

Today Firewheel Design launched the second site that I helped them develop. The first was Blinksale (the un-QuickBooks; an invoicing tool I can’t imagine doing business without). The latest is an overhaul of IconBuffet, home of their venerable stock icon collections.

Firewheel is what you’d call a “dream client.” They thoroughly get the web: they care about standards and providing real value to users. They’re massively talented designers, both in the “make it pretty” sense and the more elusive “make is usable” sense. And they provide me, as the developer, with everything I need to make a kick-ass web application.

In the case of IconBuffet, the designers at Firewheel wanted the shopping cart to be as simple as humanly possible, with instant feedback when an item is added to the cart. Ajax made perfect sense, and Rails makes that a snap. The only catch was that I needed to update three separate elements on the page any time a product was added or removed from the cart. My first solution was to write some Javascript to handle each action. My code looked like this:

<pre> var Cart = { add: function(product_id) { Element.addClassName('product_' + product_id, 'incart') new Ajax.Request('/account/add_to_cart/' + product_id, { method: 'post', onComplete: Cart.refresh }) }, remove: function(product_id) { Element.removeClassName('product_' + product_id, 'incart') new Ajax.Request('/account/remove_from_cart/' + product_id, { method: 'post', onComplete: Cart.refresh }) }, refresh: function() { new Ajax.Updater('cartbox', '/products/cartbox') new Ajax.Updater('num_items', '/products/num_items') } }</pre>

Calling Cart.add(1) would add a CSS class to a DOM element, and then send an Ajax request to the controller to add the item to the cart. That request has an onComplete callback to Cart.refresh, which made two more Ajax calls, to update the status in the sidebar and the header. It worked, but it wasn’t great: there was a noticable delay between the three changes on screen, which made the whole thing feel very sluggish. Plus, it created a slight “code smell” to have the page’s logic spread out through so many layers.

Just then, the Rails developers (notably Marcel) dropped a little goodie in my lap: RJS Templates. This addition allows you to generate Javascript from Ruby, which can be returned by Ajax calls and evaluated in the page — making problems like mine a piece of cake. Since RJS is just a couple weeks old, I suspect that IconBuffet is one of the first public apps in production to use the technique. Here’s how my code looks now:

As usual, I use link_to_remote, Rails’ standard way to create an Ajaxified link:

<pre> link_to_remote "Add to Cart", :url => { :action => 'add_to_cart', :id => product }</pre>

The controller saves the product id and renders the add_to_cart view — but instead of the usual .rhtml or .rxml template, it’s an .rjs template:

<pre> page.replace_html 'cartbox', :partial => 'cart' page.replace_html 'num_items', :partial => 'num_items' page.send :record, "Element.addClassName('product_#{@params[:id]}', 'incart')"</pre>

These three lines accomplish the same thing as the fourteen lines of Javascript above. The first line renders the ‘cart’ partial into the DOM element #cartbox. The second line does the same thing, but for the header. The third line just creates a line of Javascript to add a CSS class to an element. The results of the two techniques is the same, but the effect is far nicer now — the code is more succinct and centralized, and the user experience is significantly smoother.

There’s a whole lot more that’s possible, but this should whet your appetite. RJS isn’t yet available in any released version of Rails, so you’ve got to checkout the trunk from the repository. It’s quite a testament to the Rails core team that the bleeding-edge trunk is stable enough to build a production application on… thanks guys!