06 June 2006
Introducing Unobtrusive Javascript for Rails
Rails makes a lot of things easier for a developer. One of those things is AJAX. The built-in Javascript and AJAX helpers, such as form_remote_tag and link_to_remote, make developing AJAX apps a breeze. But if there has every been one major bone of contention with these helpers, it's the markup they produce. Developers have been well aware of the need to separate content from presentation with CSS for a while now - less prevalent is the recognition of the benefits in separating behaviour from content.
Update 21/08/2006: The latest version of this plugin is 0.3 - please see this post and the official UJS website for more information.
Rails makes a lot of things easier for a developer. One of those things is AJAX. The built-in Javascript and AJAX helpers, such as form_remote_tag and link_to_remote, make developing AJAX apps a breeze. But if there has every been one major bone of contention with these helpers, it's the markup they produce. Developers have been well aware of the need to separate content from presentation with CSS for a while now - less prevalent is the recognition of the benefits in separating behaviour from content.
I'm not going to cover the ins and outs of why you should separate behaviour from content here, but or those unfamiliar with the reasons and benefits, Peter-Paul Koch of QuirksMode published a great article in 2004 highlighting the benefits of separating behaviour from content. More recently, Dan Webb from Vivabit published an article about the problem with Rails AJAX helpers.
As Dan points out in his article, the subject has been mentioned on several occasions on the RubyOnRails mailing list but has often been shot down for a multitude of reasons ranging from "its not worth it" to "it would be too awkward". Invitations to submit patches were made but nothing came about.
So I'd like to present to you something I've been working on here at Agile Evolved - Unobtrusive Javascript for Rails. The plugin makes use of a Javascript library called event:Selectors by Justin Palmer at EncyteMedia. Similar to Ben Nolan's behaviour.js library but making full use of Prototype, it allows you to use CSS selectors to attach Javascript events to your page. This plugin allows you to make use of the event:Selectors library, but in Ruby, directly from your controller or view and have the resulting behaviour rules dynamically generated at runtime in an external javascript file. Let me demonstrate:
First of all, you'll need to grab the plugin from the Subversion repository. Please note - the plugin is in its early stages at the moment and is likely to be updated quite a bit over the coming weeks - I do not recommend using this in a production site just yet. In the meantime, if you want to have a play I recommend using svn:externals to make sure you keep your copy of the plugin up-to-date.
$ ./script/plugin install -x http://source.ujs4rails.com/current/unobtrusive_javascript
Next, you'll need to load in the Prototype javascript and the Unobtrusive Javascript scripts. Somewhere between your layout's head tags, add the following:
<%= javascript_include_tag :defaults %>
<%= unobtrusive_javascript_files %>
You can now attach events to elements in your page from either your controller or your view, using the register_js_behaviour() function. The function takes two parameters; the first is the CSS selector and event (for more details see the event:Selectors documentation) to attach your behaviour to and the second is a string of Javascript that you want to execute. Here's a small example:
<% register_js_behaviour "#my_funky_link:click", "alert('Hello World')" %>
Of course, writing out large strings of javascript could become cumbersome. The first solution is use the built-in Rails helpers to generate your Javascript strings. For instance, if we want to highlight a div when we hover over it with the mouse, you could do the following:
<% register_js_behaviour "#my_funky_div:mouseover",
visual_effect(:highlight, "my_other_div", :duration => 0.5) %>
Now, if you load up a page with one of the above calls and View Source, you might expect to see a whole load of generated Javascript. Except you won't - thats because the unobtrusive_javascript plugin makes use of a special controller and view to dynamically generate your behaviour rules at runtime, which are then linked to using a normal script tag. That means you can attach as many behaviours to your page as you like, from anywhere in your view or controller, without clogging up your rendered HTML with Javascript.
Next, there is the issue of graceful degradation. The two primary candidates for graceful degradation are the link_to_remote and form_remote_tag helpers - links and forms both have natural fallbacks - the HREF and ACTION respectively, and by using some unobtrusive Javascript we can make sure those fallbacks are used when somebody has Javascript disabled.
This initial release contains an updated version of the form_remote_tag helper - by default it does exactly the same thing as the built-in Rails helper but making it use unobtrusive Javascript is a case of one small addition to your code:
<%= form_remote_tag :url => { :action => 'foo' }, :unobtrusive => true %>
Because the event:Selector library depends on an HTML ID to attach an event, your forms will need an ID but worry not - another modification to the form_remote_tag helper will mean that if you do not specify your own HTML ID, one will be automatically generated for you.
Finally, some of you may be aware that Dan Webb, who I mentioned earlier in this article, has been working on his own unobtrusive javascript plugin. I have been in touch with Dan and he preferred the simpler syntax that my plugin uses. That said, he's plugin contains some very cool stuff and we've agreed to merge the best of both of our plugins together over the coming weeks, including the ability to supply the register_\javascript_behaviour() function with a block which will let you write the Javascript functionality you want to attach using Dan's RJS-style JavascriptProxy classes.
Please do download the plugin and have a play around. You can report any bugs on the Agile Evolved Open Source Software website and any opinions and ideas are welcomed.
Return to home page | Check out my tumblelog
14 Comments on this article
Return to home page | Check out my tumblelog
Commenting on this article is now closed
1. Comment by jim on 07 Jun 2006 at 03:06
Good job on this. I was looking for a cleaner way to use something like behavior.js and this will fill that nicely.
I also thought others may like to hear about this so I just added you to the planetrubyonrails feeds.
2. Comment by Jeroen on 07 Jun 2006 at 13:06
Cool! Very nice to see this is finally getting tackled.
Can you say something about performance and/or caching issues?
3. Comment by Jeroen on 07 Jun 2006 at 14:06
Just a small note: I had to copy event-selectors.js to public/javascript for things to work.
4. Comment by Luke on 07 Jun 2006 at 14:06
Hi Jeroen,
We haven’t really had the chance to put it through its paces – if you have any caching problems let me know.
There shouldn’t be any need to copy event-selectors.js to public/javascript – its automatically served up as part of the unobtrusivejavascriptfiles function in your layout (the same plugin controller that generates the dynamic behaviours also reads the contents of event-selectors.js and serves them up – eliminating the need to copy it over).
5. Comment by Jeroen on 07 Jun 2006 at 15:06
Thanks Luke!
Hmm, well it really didn’t work just “out of the box” only after I copied the .js file it worked. I can probably provide you with a better bug report, can I submit stuff to your Trac?
It did say this after install: Checked out revision 23. Plugin not found: unobtrusive_javascript
6. Comment by Luke on 07 Jun 2006 at 15:06
Jeroen, feel free to submit bugs to the Trac.
As it is, I’ve identified and fixed the bug – the controller action that serves up the event-selector.js was indeed looking for it in the wrong place (public/javascripts instead of my plugins assets folder). Run an svn update and you should find it works without the need for event-selector.js in the public/javascripts folder).
7. Comment by Sebastian Friedrich on 07 Jun 2006 at 16:06
great. thanks so very much for working on this. imo, this is the single most needed feature in Rails today.
Also, great to hear that you pan to pool efforts with Dan Webb. It’s a smart choice not to let this balkanize too much and instead combine the best of all into one great solution—which will hopefully be on the fast-track into Core. Thanks again.
8. Comment by rick on 07 Jun 2006 at 17:06
“single most needed feature in Rails”? I think not :)
Still, this looks really cool. I have a few questions though. Is the dynamically generated CSS cached in anyway? Are proper etag/last modified headers sent? What benefit is moving all this out of the main HTML if the user agent can’t cache the CSS? It seems like it’d have to download the same amount of bytes, but with an extra HTTP request.
It’d be nice if registerjsbehaviour could accept a block:
<% registerjsbehaviour ’#selector’ do |page| page[:foo].visualeffect :dropout end -%>
I would probably use registerevent or registerjsevent instead of registerjs_behaviour, personally. I take it an early version was based on the behaviour lib?
Anyways, this looks good. I hope to use this on Rails Weenie, which is still using the behaviour lib and my pre-rjs javascript templates. If/when I do I’ll see about sending any patches your way.
9. Comment by Luke on 07 Jun 2006 at 17:06
Hi Rick, thanks for your comments. I can tell you now that one of the plans is to allow the registerjsbehaviour function to accept a block – in fact, as we’ve started using the plugin in one of our apps we’ve really seen a use for it – we’ll combine the ability to accept a block with Dan Webb’s JavascriptProxy extensions.
The API is also not finalised – I think you might be right that using the word event instead of behaviour is a better choice but I’ll have to think about it.
In your second paragraph I’m assuming you mean JS instead of CSS – I’ve not really looked into the issue of caching at this stage but its definately something I’m bearing in mind.
10. Comment by Sean on 10 Jun 2006 at 22:06
Great stuff, the whole javascript problem has been a real pain in my side. One thing thats still bugging me is how the behavior is stored, in a session. Doesn’t that get expensive when the same data is being stored for every single user? There is probobly something im missing but i would really like this cleared up. Thanks and keep up the good work!
11. Comment by Luke on 11 Jun 2006 at 12:06
Sean, I haven’t really benchmarked it so I couldn’t say for certain. I do have some ideas for caching in mind though so stay tuned.
12. Comment by Philip Schalm on 19 Jun 2006 at 19:06
I, as well, needed to copy event-selectors.js over. Could it be because my rails app is in a sub-directory on the server, rather than it’s own directory?
13. Comment by Aditya Rajgarhia on 21 Jul 2006 at 07:07
Hi Luke,
This is a great idea for a plugin. I’ve been looking for something like this in rails! Unfortunately I have been unable to make the plugin work. I followed the instructions above, but for some reason the javascript doesn’t work (for example there is no mouseover). I’ve obviously changed the CSS id to mine. In fact I’ve tried with several ids. The WEbrick server logs tell me that /unobtrusive_javascript/generate is infact being fetched ok.
In the plugin controller, I did a “puts” for the @behaviour object and that too has the correct behaviour that I passed using registerjsbehaviour. Can you help me get this running? I would greatly appreciate it.
Thanks, and once again – good work!
14. Comment by Jesper Rønn-Jensen (justaddwater.dk) on 22 Jul 2006 at 14:07
Luke, i presume that your plugin does not modify the built in Rails javascript helpers: AutoComplete.
In my opinion, the autocompleter is still a pain in the eye with respect to unobtrusive javascript. It would be elegant to modify your plugin to use the same technique to overwrite the default autocompleter.
Please let me hear your thoughts on this: Could it be done,? Could you do it?
Thanks for sharing this plugin, I’m looking forward to use it!