March 7th | SXSW bound
I'm headed to Austin this week for SXSW Interactive—if you're there, please introduce yourself!

I'm headed to Austin this week for SXSW Interactive—if you're there, please introduce yourself!
XML.com just published an article I wrote on What’s New In Prototype 1.5. It outlines the major themes of the new release: drastically improved test coverage, a completely revamped website (now with API docs!), and oodles of fixes. But the bulk of the article is reference-style, with descriptions and examples for all of the new methods, classes, and features added since 1.4. By and large, that content was extracted from Prototype Quick Reference, which itself was extracted from Ajax on Rails. I’m grateful to O’Reilly for letting me share that content with the community.
One thing I didn’t mention in the article was the impressive degree of community involvement in this release. Prototype has been criticized in the past for having a “black box” style of development, but I think this release signals a renewed openness in the process. There is now a group of incredibly talented folks helping to shepherd the project, and I’m pretty confident that it will keep going strong. I’d like to publicly thank everybody who chipped in to this release, so I compiled a list of the top contributors (those with more than one credit in the changelog):
Next time time you see one of these guys, you owe them a pint!
Christmas came a few days early for me, as the FedEx man just dropped off my author's copy of Ajax on Rails. It has taken a tremendous amount of work over the last year, so I'm extremely gratified to finally see it in print.
If you're interested in Rails, Ajax, or you just like books, you can buy mine. It's available from Amazon, or from O'Reilly, or from any fine bookseller, once they get their shipments. Just ask for the Monkey Book.*
If you like the idea of books but not the physical reality of them, you're not out of luck. O'Reilly decided that Chapter 10, a reference to the Prototype JavaScript library, was useful enough to be packaged as a standalone PDF. So even if you're not a Rails user, you might be interested in the Prototype Quick Reference. When Prototype 1.5 is released (soon), we'll release a new edition covering any last-minute changes; but if you buy it now, you'll get the upgrade for free. Chapter 11 (aka Script.aculo.us Quick Reference) will be released in a similar manner soon.
I owe a huge thanks to everyone who helped make this book—all of the gifted folks at O'Reilly, my peerless peer reviewers, everyone who has worked on Rails, Prototype, and Scriptaculous, and my friends, parents, and my wife. I'm a lucky guy to be surrounded by such inspiring people, and I'm quite sure I would have given up on this project without your encouragement.
*Actually, don't do that; they won't know what you mean at all.
Just a quick note to point out this article in the current issue of The Christian Century magazine: A visit to Jacob's Well. The author does a good job of portraying what Jacob's Well is about—and it reminded me of how grateful I am to be part of this community.
I'm happy to announce that Firewheel just released the Blinksale API. This is a project I've been working on for quite a while, and it feels great to release it to the world. I'm excited to see what's done with it. At some point I plan to write up what we learned building this sucker, but for now I just want to get the word out.
So if you're into this sort of thing, check it out and let me know if there are any questions, problems, or suggestions!
Progress on my upcoming O’Reilly book, Ajax on Rails, is moving fast. That means it’s time to bring a few more people into the project, as Tech Reviewers. The idea of a tech review isn’t to worry about typos or grammar, but to make sure I’ve covered all of the relevant topics thoroughly and accurately.
You don’t need to be an expert on Ajax, Rails, or writing—just interested in the subject and willing to give constructive criticism. The schedule is getting crunched, so we need feedback within a week or two.
Interested? Email me at sco@scottraymond.net.
We’ve got our reviewers. Thanks to all those who replied.
Inspired largely by DHH’s RailsConf keynote, I recently set about refactoring IconBuffet.com. Specifically, I wanted to embrace the SimplyRestful plugin and see how much of the application could be re-cast in terms of resources, as opposed to RPC—without changing any functionality.
The process was gradual, but as of today it’s basically done, so I thought I’d summarize the results. Before refactoring, IconBuffet had ten controllers and seventy-six actions:
Now, without adding or removing any features, IconBuffet has thirteen controllers and fifty-eight actions:
So, by adding a few controllers, I cut the total number of actions by almost twenty. That’s a pretty big deal, because actions are like moving parts in a machine—the more there are, the more can go wrong. The fact that I could cut almost twenty actions indicates that there was a lot of redundancy hiding beneath the surface. A big part of that redundancy was in having an Admin module, which quickly proved unnecessary. Instead, I just use before_filters to protect certain actions.
Cutting actions is great, but even more significant is that the remaining ones are almost completely uniform. There are seven standard Rails actions: index, new, create, show, edit, update, and destroy. Everything else—oddball actions—are usually a clue that you’re doing RPC. In the old version, there were forty oddball actions; now there are only five (and four of those are essentially static pages in the about controller.) The upshot is that the controllers are very uniform, which makes the entire application conceptually simpler, and thus easier to maintain, test, and extend.
Perhaps the most dramatic example of that simplification is in the Routes configuration. Take a look at the before and after:
<strong># Old: 16 lines, giving 8 named routes</strong>
ActionController::Routing::Routes.draw do |map|
map.with_options :controller => 'products' do |m|
m.product 'products/:slug', :action => 'product'
m.category 'categories/:slug', :action => 'category'
m.download 'products/:slug/download', :action => 'download'
m.add 'add/:id', :action => 'add_to_cart'
m.remove_cart 'remove/:id', :action => 'remove_from_cart'
end
map.with_options :controller => 'delivery' do |p|
m.connect 'delivery', :action => 'index'
m.receive 'delivery/receive/:token', :action => 'receive'
m.connect 'delivery/:action',
m.delivery 'delivery/:slug/:action', :action => 'product'
end
map.connect 'admin', :controller => "admin/products", :action => "dashboard"
map.home '', :controller => "about", :action => "home"
map.connect ':controller/:action/:id'
end
<strong># New: 3 lines, giving 89 named routes</strong>
ActionController::Routing::Routes.draw do |map|
map.resources *%w( product category person cart_item
order deliverable delivery push
specification session setting )
map.home '', :controller => "about", :action => "home"
map.connect ':controller/:action/:id'
end
Now that’s the beauty of convention over configuration. Because the actions are uniform, the routes get far more bang for the buck. Count ’em: eighty-nine named routes from just three lines of configuration.
It’s been observed that the success of Ruby on Rails isn’t due to any particular technical breakthrough or insight. The framework is basically just a collection of good ideas culled from elsewhere, packaged nicely and explained well, with a heapin’ helping of hype. One of those good ideas is passive code generators: tools that spit out application boilerplate for the developer to modify as needed. On the surface, code generators are simply about cutting keystrokes. But there are subtler implications as well: they lower the barrier to entry for new developers, they encourage consistency (both intra- and inter-project), and they create invitations to best practices, like unit testing.
After installation, code generation is the first thing a new Rails developer experiences: rails myapp. (Unfortunately, the next step for newcomers is usually script/generate scaffold, but that’s another story.) The generators are a large part of what makes Rails welcoming to first-timers, and pleasant for full-timers. And they cement community conventions, which is a huge benefit—every Rails app looks the same in terms of directory structure, file names, and configuration format. Learn how one Rails app is organized, and you’ve learned how they’re all organized.
Bells ring; singers sing; abstractions leak. For many Railsers—even experienced ones—generators involve some degree of voodoo. They make it easy to not understand how everything works. What are all those files? What’s actually required, and what’s just convention? The significance isn’t just academic. Someday, you’ll face a bug that requires understanding the generated code.
So let’s void the warranty, crack the case, shun the scripts, and build a Rails app from scratch. You’ll find it’s not hard. A minimal working Rails application needs only six directories, five files, and 19 lines of code—including configuration. Here’s how to do it.
I’ll assume that you’ve got Rails installed and working already (if not, see Curt’s introduction.) I’ll also assume MySQL is installed and accessible with the default permissions (username root; blank password). And we’ll use the excellent Mongrel web server (gem install mongrel) to serve the app. The command line examples are for a Unix-like OS, but the ideas transfer directly to Windows. This example will expect a database named myapp, with one table named people, with one column named name. Go ahead and create a row in the table, with your name in it. Ready? Set?
mkdir myapp; cd myappmkdir log public app config app/controllers config/environmentstouch config/environments/development.rbrequire 'rubygems' require_gem 'rails' require 'initializer' RAILS_ROOT = File.join File.dirname(__FILE__), '..' Rails::Initializer.run
These lines pull in the framework (through the RubyGems package manager) and wind everything up. A stock environment.rb includes much more, providing configuration hooks and code to handle edge cases.
development: adapter: mysql database: myapp
Rails will assume reasonable defaults: root for the username, a blank password, and localhost for the host.
ActionController::Routing::Routes.draw do |map| map.connect '', :controller => 'people' end
class Person < ActiveRecord::Base; end class PeopleController < ActionController::Base def index @person = Person.find :first render :inline => "Hello, <%= @person.name %>" end end
Notice we don’t have an ApplicationController—it’s optional; your controllers can inherit directly from ActionController::Base. And we don’t have the models, helpers, or views directories. They aren’t needed; models can be defined anywhere, including application.rb.
That’s it—the minimal application, stripped it down to the rails. Moment of truth time: start it up by running mongrel_rails start, and then point your browser to http://localhost:3000/. If all is well, you’ll see a hyper-minimal Rails app that exercises the whole stack, M, V, and C. Now stop to consider what’s not there: helpers, views, and model files, a bunch of directories (components, db, doc, lib, script, and vendor), dispatchers, Rake tasks, and tests. All of those things are technically optional. They’re conventional, convenient, and recommended—but not essential.
So is this the real man’s way to write Rails? Hell no. Rails’ generators are there for a reason, and there’s no reason to forgo them in your real work. But it’s a worthwhile exercise to go without them, at least once. Not only will you appreciate the shortcuts more, but your understanding of the framework will be stronger.
Have I overlooked anything else that can be cut? Left out something essential? Let me know!
P.S. Attending RailsConf this weekend in Chicago? I’d love to meet. I’ll be speaking at 10am Saturday morning—please stop by and introduce yourself!

I’m proud to have an article published in the July issue of Linux Journal, entitled “Rails Writ Large”. The editors at LJ asked me to summarize the new additions to Rails in 1.1. Of course, there was so much new that I couldn’t possibly mention everything in the space available, but I hit the highlights. I was also able to include a quick overview of Rails for newcomers—and hopefully just enough sample code to entice them to dig deeper.
And speaking of digging deeper, the mini-bio at the end of the article reveals something I’ve been keeping quiet: I am writing a book for O’Reilly on creating Ajax-powered applications with Rails, to be available later this year. If you have started using Rails or are considering it for Ajax development, this book will have you covered from every angle—from the nuts and bolts of XMLHttpRequest, through Prototype, Scriptaculous, and RJS, and up to the higher-level issues of testing, design, and usability. It’s been a challenging and rewarding project so far, and I’m really looking forward to sharing the result with the world.
But more on that in the future. For now, if you want to start your collection of the complete works of Scott Raymond, head to your local purveyor of fine magazines and look for DHH’s mug.

Just a quick note to mention that Rails 1.1 is real now, so I’ve once again updated my article summarizing the changelogs, and I’ve added sections for Prototype and Scriptaculous as well.
I have updated my last post on the upcoming changes in Rails 1.1 to reflect the last couple weeks of development. The most notable changes center around support for interesting HTTP capabilities—it seems that David has gotten some REST religion, which I’m pleased to see. Several of these new features have already made it into the development branch of Blinksale 2.0.
The updates to the article are scattered throughout, so if you just want the highlights, here are the keywords to look for:
render(:xml => ...)respond_torender :content_type => ...to_xmlreload! and app in the consoletest:uncommittedgroup_byin groups ofIt’s been just over two months since the Rails 1.0 milestone, and the long push of testing and refining that lead up to it. Surely, the contributors have been taking a much-deserved rest in the time since then. Surely?
In fact, the core team (and over 120 other contributors) haven’t slowed down one bit, and the next major release of Rails is here. If you’re running Edge Rails, you already have access to all the latest features, but perhaps a few have missed your radar. So I’d like to round-up what’s new since 1.0 (or at least, everything that’s interesting to me—I’ve skipped a ton of bug fixes, performance improvements, environment-specific enhancements, and smaller changes.) Let’s start with the easier parts.
:ruby instead of :sql. This wins the award for best changelog comment in 1.1:This means that we’ll assume you want to live in the world of db/schema.rb where the grass is green and the girls are pretty… Brought to you by the federation of opinionated framework builders!
-r/--repeat option to script/process/spawner.-c/--config option on script/server allows you to specify a path to your lighttpd.conf.ENV["RAILS\_ENV"] = "production" in config/environment.rb doesn’t wreak havoc. (I’ve been bitten by that nasty.)javascript\_include\_tag :defaults. (I’d love it if the generator also created a blank application.css, and an app/views/layouts/application.rhtml with the standard XHTML boilerplate.)reload! reloads all models, and app is an accessor for an instance of Integration::Session. Handy!>> puts helper.options_for_select([%w(a 1), %w(b 2), %w(c 3)])
option value="1">a</option>
option value="2">b</option>
option value="3">c</option>
> nil
load_fixtures is now db:fixtures:load (which you can also use to load a subset of the application’s fixtures, e.g. rake db:fixtures:load FIXTURES=customers,plans). All the old task names will still work. Run rake --tasks to see the new task names.test:uncommitted tests changes since last checkin to Subversion.ActionController::Routing::Routes.draw do |map|
# Account routes
map.with_options(:controller => 'account') do |account|
account.home '', :action => 'dashboard'
account.signup 'signup', :action => 'new'
account.logout 'logout', :action => 'logout'
end
end
[1,2,3].to_json => "[1, 2, 3]"
"Hello".to_json => "\"Hello\""
Person.find(:first).to_json =>
"{\"attributes\": {\"id\": \"1\", \"name\": \"Scott Raymond\"}}"
transcripts.group_by(&:day)
%w(1 2 3 4 5 6 7).in_groups_of(3) {|g| p g}
["1", "2", "3"]
["4", "5", "6"]
["7", nil, nil]
5.minutes + 30.seconds instead of 5.minutes + 30.logger.around_info("Start rendering component (#{options.inspect}): ",
"End of component rendering") { yield }
Time.now.beginning_of_quarter => Sun Jan 01 00:00:00 CST 2006
class Account < ActiveRecord::Base
has_one :subscription
delegate :free?, :paying?, :to => :subscription
delegate :overdue?, :to => "subscription.last_payment"
end
account.free? # => account.subscription.free?
account.overdue? # => account.subscription.last_payment.overdue?
Now for the fun stuff!
class Author < ActiveRecord::Base
has_many :authorships
has_many :books, :through => :authorships
end
class Book < ActiveRecord::Base
has_many :authorships
has_many :authors, :through => :authorships
end
class Authorship < ActiveRecord::Base
belongs_to :author
belongs_to :book
end
Author.find(:first).books.find(:all, :include => :reviews)
class Firm < ActiveRecord::Base
has_many :clients
has_many :invoices, :through => :clients
end
class Client < ActiveRecord::Base
belongs_to :firm
has_many :invoices
end
class Invoice < ActiveRecord::Base
belongs_to :client
end
class Address < ActiveRecord::Base
belongs_to :addressable, :polymorphic => true
end
class User < ActiveRecord::Base
has_one :address, :as => :addressable
end
class Company < ActiveRecord::Base
has_one :address, :as => :addressable
end
Developer.with_scope(:find => { :conditions => "salary > 10000", :limit => 10 }) do
Developer.find(:all) # => SELECT * FROM developers WHERE (salary > 10000) LIMIT 10
# inner rule is used. (all previous parameters are ignored)
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
Developer.find(:all) # => SELECT * FROM developers WHERE (name = 'Jamis')
end
# parameters are merged
Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
Developer.find(:all) # => SELECT * FROM developers WHERE (( salary > 10000 ) AND ( name = 'Jamis' )) LIMIT 10
end
end
Person.count
Person.average :age
Person.minimum :age
Person.maximum :age
Person.sum :salary, :group => :last_name
Author.find(:all, :include=> { :posts=> :comments }), which will fetch all authors, their posts, and the comments belonging to those posts in a single query. For example:Author.find(:all, :include=>{:posts=>:comments})
Author.find(:all, :include=>[{:posts=>:comments}, :categorizations])
Author.find(:all, :include=>{:posts=>[:comments, :categorizations]})
Company.find(:all, :include=>{:groups=>{:members=>{:favorites}}})
find calls on has\_and\_belongs\_to\_many and has\_many assosociations. For example:class Post
has_many :recent_comments, :class_name => "Comment", :limit => 10, :include => :author
end
post.recent_comments.find(:all) # Uses LIMIT 10 and includes authors
post.recent_comments.find(:all, :limit => nil) # Uses no limit but include authors
post.recent_comments.find(:all, :limit => nil, :include => nil) # Uses no limit and doesn't include authors
to_xml. For example:topic.to_xml
topic.to_xml(:skip_instruct => true, :skip_attributes => [ :id, bonus_time, :written_on, replies_count ])
firm.to_xml :include => [ :account, :clients ]
validate\_uniqueness\_of to be scoped by multiple columns. See this.:exclusively\_dependent option has been deprecated in favor of :dependent => :delete_all..find() method, and the has\_and\_belongs\_to\_many and has\_many associations, now all take :group, :limit, :offset, and :select options.:conditions. See this.validates\_length\_of now works on UTF-8 strings—it counts characters instead of bytes.The RJS templates are passed an page object that represents the JavaScriptGenerator, which has many tricks up its sleeve:
alert 'Howdy'redirect_tocallassignreplaceinsert\_html :bottom, 'list', '<li>Last item</li>'visual\_effect :highlight, 'list'show 'status-indicator'hide 'status-indicator', 'cancel-link'['blank\_slate']['blank\_slate'].show # => $('blank_slate').show();select('p')select('p.welcome b').first # => $$('p.welcome b').first();select('p.welcome b').first.hide # => $$('p.welcome b').first().hide();<<draggable 'product-1'drop_receiving 'wastebasket', :url => { :action => 'delete' }sortable 'todolist', :url => { action => 'change_order' }delay(20) { page.visual_effect :fade, 'notice' }
* RJS exception notification via alert() (set config.action_view.debug_rjs = true)page.select('#items li').collect('items'){ |element| element.hide } generates var items = $$('#items li').collect(function(value, index) { return value.hide(); });class UserController < ApplicationController
def refresh
render :update do |page|
page.replace_html 'user_list', :partial => 'user', :collection => @users
page.visual_effect :highlight, 'user_list'
end
end
end
module ApplicationHelper
def update_time
page.replace_html 'time', Time.now.to_s(:db)
page.visual_effect :highlight, 'time'
end
end
class UserController < ApplicationController
def poll
render :update { |page| page.update_time }
end
end
respond_to lets an action output different formats according to the HTTP Accept header. In other words, you’ve got instance REST web services. Blinksale 2.0 already uses this. For example:class WeblogController < ActionController::Base
def index
@posts = Post.find :all
respond_to do |wants|
wants.html # using defaults, which will render weblog/index.rhtml
wants.xml { render :xml => @posts.to_xml } # generates XML and sends it with the right MIME type
wants.js # renders index.rjs
end
end
end
# Assign a new param parser to a new content type
ActionController::Base.param_parsers['application/atom+xml'] = Proc.new do |data|
node = REXML::Document.new(post)
{ node.root.name => node.root }
end
# Assign the default XmlSimple to a new content type
ActionController::Base.param_parsers['application/backpack+xml'] = :xml_simple
<% form_for :person => @person, :url => { :action => "update" } do |f| %>
First name: <%= f.text_field :first_name %>
Last name : <%= f.text_field :last_name %>
Biography : <%= f.text_area :biography %>
Admin? : <%= f.check_box :admin %>
<% end %>
<% form_for :person => person, :url => { :action => "update" } do |person_form| %>
First name: <%= person_form.text_field :first_name %>
Last name : <%= person_form.text_field :last_name %>
<% fields_for :permission => person.permission do |permission_fields| %>
Admin? : <%= permission_fields.check_box :admin %>
<% end %>
<% end %>
form_for and friends can take a * option, where you can pass a custom subclass of FormBuilder. For example:<% form_for :person, @person, :url => { :action => "update" }, :builder => LabellingFormBuilder do |f| %>
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
<% end %>
require "#{File.dirname(__FILE__)}/test_helper"
require "integration_test"
class ExampleTest < ActionController::IntegrationTest
fixtures :people
def test_login
# get the login page
get "/login"
assert_equal 200, status
# post the login and follow through to the home page
post "/login", :username => people(:jamis).username,
:password => people(:jamis).password
follow_redirect!
assert_equal 200, status
assert_equal "/home", path
end
end
Integration Tests can also have multiple session instances open per test, and even extend those instances with assertions and methods to create a very *powerful testing DSL that is specific for your application. You can even reference any named routes you happen to have defined. For example (think Campfire here):
def test_login_and_speak
jamis, david = login(:jamis), login(:david)
room = rooms(:office)
jamis.enter(room)
jamis.speak(room, "anybody home?")
david.enter(room)
david.speak(room, "hello!")
end
private
module CustomAssertions
def enter(room)
# reference a named route, for maximum internal consistency!
get(room_url(:id => room.id))
assert(...)
...
end
def speak(room, message)
xml_http_request "/say/#{room.id}", :message => message
assert(...)
...
end
end
def login(who)
open_session do |sess|
sess.extend(CustomAssertions)
who = people(who)
sess.post "/login", :username => who.username,
:password => who.password
assert(...)
end
end
render(:xml => xml) works just like render(:text => text), but sets the content-type to application/xml and the charset to UTF-8.:content_type option to render, so you can change the content type on the fly. For example:render :action => "atom.rxml", :content_type => "application/atom+xml"
auto_link(post.body) { |text| truncate(text, 10) }
content\_for and capture now work in .rxml (and any non-rhtml template). See this.visual\_effect supports scoped queues. See this.observe\_field now has an n option to specify a different callback hook to have the observer trigger on.link\_to\_function, but uses a button instead of a link.link\_to\_function will now honor existing :onclick definitions when adding the function call.submit\_tag now has a h option to change the text of disabled submit buttons.visual\_effect can now toggle visual effects. See this.auto\_complete\_field now has a t option for to only use part of the auto-complete suggestion as the value for insertion.$$ function) matches elements by CSS selector tokens. For example:// Find all <img> elements inside <p> elements with class
// "summary", all inside the <div> with id "page". Hide
// each matched <img> tag.
$$('div#page p.summary img').each(Element.hide)
// Attributes can be used in selectors as well:
$$('form#foo input[type=text]').each(function(input) {
input.setStyle({color: 'red'});
});
$ and $$, so you can now write $('foo').show() instead of Element.show('foo').truncate, gsub, sub, scan, and strip.Element.childOf(element, ancestor) returns true when element is a child of ancestor.Just a quick note to mention that I’m the guest on the latest Ruby on Rails Podcast. Here’s the direct link to the MP3 (22mb). Thanks for having me, Geoffrey!
Congratulations to the Rails core team on today’s long-awaited release of 1.0. The new website is fittingly spare and elegant.
The release almost exactly coincides with my one-year anniversary of using Rails, and it’s been a good year indeed. It’s quite a thrill to have not one, but two applications that I developed for Firewheel listed on the showcase page. Thanks for that honor, and bravo, gents.
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:
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')
}
}
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:
link_to_remote "Add to Cart", :url => { :action => 'add_to_cart', :id => product }
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:
page.replace_html 'cartbox', :partial => 'cart'
page.replace_html 'num_items', :partial => 'num_items'
page.send :record, "Element.addClassName('product_#{@params[:id]}', 'incart')"
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!