The idea with unobtrusive Javascript in to separate markup (HTML) from behavior (Javascript), just as CSS help us separate styling from markup. By disallowing Javascript in your markup, you gain a better overview of where to find what in your code base. Behavior goes to static javascript files and markup goes to template files! There are several other benefits to this technique that I will not be covered here.
As you might have heard Rails 3 is actually adopting this idea, which will spare us for lots of ugly repeating Javascript code when using Rails form and link helpers. The release of Rails 3 is still far away, so in this article I’ll show you how to implement the technique in Rails 2.
I recommend that you have some understanding of the concept of Unobtrusive Javascript before reading on.
The problem
When using Rails’ built in link_to_remote like so:
<%= link_to 'Cancel my subscription', :url => @subscription, :method => :destroy %>
... we get something like this:
<a href="/subscriptions/1" onclick="var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'destroy'); f.appendChild(m);var s = document.createElement('input'); s.setAttribute('type', 'hidden'); s.setAttribute('name', 'authenticity_token'); s.setAttribute('value', 'XqCg0tt0NuqGVVuXtrPcued7ESetYt726rmiQ8Lp48o='); f.appendChild(s);f.submit();return false;">Cancel my subscription</a>
As you can see this markup contains behavior/javascript, which is what we want to avoid.
The solution
What we want to do is to extract the behavior and put this in a separate static javascript file. The behavior will then be applied to the page upon page initialization by using CSS selection. To implement this, I first wrote to helper method that makes it easy to send regular requests and ajax requests:
function send_remote_request(url, verb) {
new Ajax.Request(url, {
parameters: { 'authenticity_token': authenticity_token },
method: verb
});
}
function send_request(url, verb) {
var form = new Element('form', {
action: url,
method: verb
}).hide();
form.insert(new Element('input', {
name: 'authenticity_token',
value: authenticity_token
}));
if(verb != 'post') {
form.insert(new Element('input', {
name: '_method',
value: verb
}));
}
document.body.insert(form);
form.submit();
}
As you can see these helpers depend on Prototype. It also depend on the authenticity_token, which is required by Rails for non get requests. I’ll get back to that later.
Next, I wrote the piece of code that will actually hook into your DOM and apply the behavior:
document.observe("dom:loaded", function() {
var verbs = ['post', 'delete', 'put'];
document.observe('click', function(e) {
var elm = e.element();
verbs.each(function(verb) {
if(elm.hasClassName(verb)) {
if(elm.hasClassName('remote')) {
send_remote_request(elm.href, verb);
} else {
send_request(elm.href, verb);
}
e.stop();
return false;
}
});
});
});
When the page/DOM has loaded, we register and add a callback on the click event of the entire document. This means that the callback function is called every time the user clicks anywhere on the page. We loop through all the three http verbs post, delete, and put. If a match is found, we fire either a normal request or a ajax request depending on whether or not the target element (the clicked element – for example an a tag) has the CSS class name remote. When the request is sent, we cancel normal behavior by calling the stop() method on the event object, e.
In order for this to work you must add both of the above pieces of Javascript in your public/javascripts/application.js file and include it like this:
<%= stylesheet_link_tag 'default' %> <script type="text/javascript"> var authenticity_token = '<%= form_authenticity_token %>'; </script>
In order to let send_request() and send_remote_request() work for non get requests we need to make our authenticity_token available through Javascript. This is due to a security mechanism of Rails. When all of this is setup you can now write things such as:
<%= link_to 'Cancel my subscription', @subscription, :class => 'destroy' %> <%= link_to 'Cancel my subscription via ajax', @subscription, :class => 'remote destroy' %> <%= link_to 'Create a subscription', subscriptions_path, :class => 'remote post' %>
... which will generate nice and clean HTML for you:
<a href="/subscriptions/1" class="destroy">Cancel my subscription</a> <a href="/subscriptions/1" class="remote destroy">Cancel my subscription via ajax</a> <a href="/subscriptions" class="remote post">Create a subscription</a>
This output is much cleaner than what Rails 2 helpers give you (as shown in the example in the beginning of this article).
I hope you enjoyed the article and learned something or two about coding Javascript unobtrusively.