Unobtrusive JavaScript in Rails 3

This article targets Rails 3

The article was written as of Rails 3.0. The information contained in this page might not apply to different versions.

One of the biggest changes on the frontend side of the upcoming Rails 3 version is the replacement of the Prototype JavaScript helpers in favor of Unobtrusive JavaScript (UJS). The implementation of Unobtrusive JavaScript, and the consequent removal of the old inline AJAX commands, offers at least three advantages:

  • Less verbose, inline, behavior code in the HTML document, with the result of much more lightweight, cleaner and readable source code
  • Rails 3 is no longer Prototype-oriented. With Rails 3 you can easily switch from a JavaScript framework to an other.
  • Rails 3 code is now JavaScript framework agnostic. It no longer contains framework-specific commands or scripts.

Let's jump right into an example. Do you remember the link to destroy an object, generated by the link_to helper?

<%= link_to "delete", domain_path(@domain), :method => :delete, :confirm => "Are you sure?" %>

outputs

<a href="/domains/1" class="destroy" onclick="if (confirm('Are you sure?')) { 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', 'delete'); f.appendChild(m);var s = document.createElement('input'); s.setAttribute('type', 'hidden'); s.setAttribute('name', 'authenticity_token'); s.setAttribute('value', 'pKvg9hsnQ33uk='); f.appendChild(s);f.submit(); };return false;">delete</a>

The same Ruby helper call, in Rails 3, generates

<a href="/domains/1" data-confirm="Are you sure?" data-method="delete" rel="nofollow">delete</a>

This is made possible by the use of Unobtrusive JavaScript. The entire logic to handle the emulation of the HTTP DELETE method has been extracted into a separate JavaScript file and is executed when the page is loaded. The scripts containing the behavior are then attached to their corresponding JavaScript events. We'll take a closer look at that file in a few minutes.

This feature required a major rewrite of the ActionView helpers which resulted in the following changes:

  1. New helpers and files are available to add unobtrusive JavaScript support to your Rails application
  2. The ActionView::Helpers::PrototypeHelper has been heavily modified. Many legacy helpers are now available as a separate plugin
  3. All the remote_<method< helpers has been removed. To make them working with AJAX, simply pass the :remote => true option to the original non-remote method
  4. All the AJAX-specific options representing callbacks are gone. For instance, there's no :loaded, :loading or :complete option for remote link_to helper anymore.

So what?

At this point, you might be wondering what this means in practice and what you need to change in order to make your Rails 2 application working with Rails 3.

So let's go back to the list of changes and discuss each point in the list. I'm assuming you will use the jQuery framework, but the same principles can apply to all the other supported JavaScript frameworks.

1. New helpers and files

The heart of the Unobtrusive JavaScript feature in Rails 3 is the new rails.js file. When you generate a new Rails 3 application, a file called rails.js is created in the public/javascripts folder along with all the other .js files you are used to see in a Rails 2 project.

rails.js contains all the unobtrusive handlers. By default, Rails assumes you are using Prototype, but there's also an official fork for jQuery.

You need to include this file in your application to have all the unobtrusive features working correctly. Because rails.js is part of the :defaults bundle, if you are using the following statement you don't need to change anything.

<%= javascript_include_tag :defaults %>

Otherwise, you can include the files separately. This is the case, when you want a more fine-grained control over the scripts included in your application or you want to use other alternatives, such as a CDN.

<%= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js", "jquery.rails.js", "application.js" %>

Now we know about the files, but what about the helpers? If you ever worked with remote helpers in Rails 2, you are probably aware of the Rails authentication token. In Rails 3, the authentication token still exists, but it needs an unobtrusive touch.

Here comes the new csrf_meta_tag helper. It returns two HTML meta tags which include all the information necessary to support the XSS protection in Rails 3.

<meta name="csrf-param" content="authenticity_token"/>
<meta name="csrf-token" content="9SdBB/Uftw7IDQH4aKblEUcLXwvgw9vkju9N1ObyCCM="/>

Because the output is meta tags, this helper is expected to be called in the head section of your HTML page. In all likelihood, this will happen in your Rails layout files.

Here's the simplest Rails 3 layout

<!DOCTYPE html>
<html>
<head>
  <title>Foo</title>
  <%= stylesheet_link_tag :all %>
  <%= javascript_include_tag :defaults %>
  <%= csrf_meta_tag %>
</head>
<body>

<%= yield %>

</body>
</html>

As you can guess from the DOCTYPE declaration, the new default Rails layout is a HTML 5 document. Get yourself accustomed to it, unobtrusive JavaScript and HTML 5 are very good friends.

2. ActionView::Helpers::PrototypeHelper

ActionView::Helpers::PrototypeHelper has been heavily modified. The following helpers have been removed and made available as a plugin.

  • buttontoremote
  • submittoremote
  • observe_field
  • observe_form
  • periodicallycallremote
  • linktoremote (*)
  • formremotetag (*)
  • formremotefor (*)
  • remoteformfor (*)

partially supported using the :remote => true option. See section 3.

If you are using one of these tags, you might need to rewrite the logic using Unobtrusive JavaScript.

Here's an example for the observe_field helper, using jQuery. When the content of the field is changed, the browser triggers a call to the :live_search action and replaces the content of the #results element.

<%= text_field_tag :search %>
<%= observe_field :search, :url => live_search_path, :update => :results %>

In Rails 3 becomes

// Append the function to the "document ready" chain
jQuery(function($) {
  // when the #search field changes
  $("#search").change(function() {
    // make a POST call and replace the content
    $.post(<%= live_search_path %>, function(data) {
      $("#results").html(data);
    });
  });
})

The example above uses at least 3 different jQuery methods: $(document).ready(), .change(), and jQuery.post.

One additional note about the example. Placing a javascript_tag in the middle of a page is probably not so unobtrusive. I suggest you to take advantage of the content_for helper to dynamically inject the content in the head section of your page, or place all the login in a separate JavaScript file, such as application.js.

As you probably noticed, Rails 3 forces developers to have a deeper understanding of JavaScript. You gain flexibility for the price of additional development effort. From one side, this requirement makes Rails development less automatic, unlike it has been until today. From an other point of view, this requirement makes developers more aware of what they are doing.

If you are using Prototype, you might be tempted to install the legacy plugin. Don't do it. Even if it makes Rails 3 upgrade easier, you would probably continue to use legacy helpers and your application will become harder to upgrade day after day. Taking the faster and easier way is not a good idea in the long terms. Schedule a reasonable amount of time and plan the full upgrade instead.

I believe this change would encounter many disappointed developers as soon as Rails 3 will be largely available, but I'm sure it will eventually become one of the most appreciate design decision in the long term. I strongly agree with the Rails 3 development team for this choice. It also comes at the right time. If Rails 1 or Rails 2 wouldn't have included such easy way to write JavaScript from the beginning, many existing Rails developers would probably never been captured by the framework in the past.

3. remote_<method> helpers

As mentioned in the previous section, all remote_something and something_to_remote methods have been removed from Rails 3. Nonetheless, remote helpers are still available thanks to the :remote => true option.

Here's a few examples.

# Rails 2
link_to_remote "Name", url_path
# Rals 3
link_to "Name", url_path, :remote => true

# Rails 2
form_tag "/path" do
end
# Rails 3
form_tag "/path", :remote => true do
end

# Rails 2
remote_form_for @article do
end
# Rails 3
form_for @article, :remote => true do
end

Rails 3 helpers never write inline JavaScript. Instead, they use HTML 5 data attributes to store remote metadata information within the HTML element. The rails.js file contains all the behavior required to handle these attributes.

Let's use the link_to helper as example.

<%= link_to "destroy", article_path(@article), :method => :delete, :remote => true %>

The output is

<a href="/articles/1" data-method="delete" data-remote="true" rel="nofollow">destroy</a>

Notice the data-method and data-remote attributes. The first indicates you want to perform a DELETE HTTP request. The second identifies a remote request and is added whenever you use the :remote option.

<%= link_to "destroy", article_path(@article) %>
<a href="/articles/1" rel="nofollow">destroy</a>

<%= link_to "destroy", article_path(@article), :method => :delete %>
<a href="/articles/1" data-remote="true" rel="nofollow">destroy</a>

<%= link_to "destroy", article_path(@article), :remote => true %>
<a href="/articles/1" data-remote="true" rel="nofollow">destroy</a>

If you open the rails.js file, you will notice several remote handler definitions. The first one handles the case of remote form submission, the second one handles remote links and input fields, the third handles not-remote links that should behave likes form.

/**
 * remote handlers
 */
$('form[data-remote]').live('submit', function(e) {
  $(this).callRemote();
  e.preventDefault();
});

$('a[data-remote],input[data-remote]').live('click', function(e) {
  $(this).callRemote();
  e.preventDefault();
});

$('a[data-method]:not([data-remote])').live('click', function(e) {
  // ...
});

I strongly encourage you to carefully examine the content of the `rails.js*` file before actually starting with the upgrade. As I said before, this is the center of the Rails 3 unobtrusive feature and you *MUST have a good understanding of it.

Ryan Bates also created a very good screencast about upgrading remote helpers to Rails 3.

Reserved data attributes

From the rails.js file we can also extract the list of data- attributes which have a special meaning in Rails 3 and should therefore be considered reserved keys.

  • data-method
  • data-confirm
  • data-remote
  • data-disable-with

As a bonus feature, you don't a Rails helper to take advantage of them. For instance, the following example will cause a confirmation dialog to appear when the button is clicked.

<button data-confirm="Do you really want to continue?">Click me</button>

4. Remote JavaScript callbacks

One topic I have never found covered so far in all the existing Unobtrusive JavaScript Rails 3 posts is remote JavaScript callbacks. This is odd because they are largely used. More precisely, I'm talking about the :loading, :loaded, :success, etc callbacks.

Quoting the linktoremote Rails 2 documentation

The callbacks that may be specified are (in order):

:loading:Called when the remote document is being loaded with data by the browser.
:loaded:Called when the browser has finished loading the remote document.
:interactive:Called when the user can interact with the remote document, even though it has not finished loading.
:success:Called when the XMLHttpRequest is completed, and the HTTP status code is in the 2XX range.
:failure:Called when the XMLHttpRequest is completed, and the HTTP status code is not in the 2XX range.
:complete:Called when the XMLHttpRequest is complete (fires after success/failure if they are present).

You can further refine :success and :failure by adding additional callbacks for specific status codes.

Example:

# Generates: <a href="#" onclick="new Ajax.Request('/testing/action', {asynchronous:true, evalScripts:true,
#            on404:function(request){alert('Not found...? Wrong URL...?')},
#            onFailure:function(request){alert('HTTP Error ' + request.status + '!')}}); return false;">hello</a>
link_to_remote word,
  :url => { :action => "action" },
  404 => "alert('Not found...? Wrong URL...?')",
  :failure => "alert('HTTP Error ' + request.status + '!')"

A status code callback overrides the success/failure handlers if present.

These callbacks have not disappeared in Rails 3. It turns out they are now available as framework-native JavaScript events.

To better understand this point, we need to open the rails.js file again. Remember, for the purpose of this article I'm still referring to the official jQuery version.

Locate the callRemote function and search for the following lines:

beforeSend: function (xhr) {
  el.trigger('ajax:loading', xhr);
},
success: function (data, status, xhr) {
  el.trigger('ajax:success', [data, status, xhr]);
},
complete: function (xhr) {
  el.trigger('ajax:complete', xhr);
},
error: function (xhr, status, error) {
  el.trigger('ajax:failure', [xhr, status, error]);
}

Here they are, our lovely callbacks. They are now implemented as jQuery events:

  1. ajax:loading - triggered before executing the AJAX request
  2. ajax:success - triggered after a successful AJAX request
  3. ajax:complete - triggered after the AJAX request is complete, regardless the status of the response
  4. ajax:failure - triggered after a failed AJAX request, as opposite to ajax:success

If you are barely familiar with the jQuery.ajax() method, you already noticed that these custom Rails callbacks are very close to the original jQuery AJAX callback functions.

At this point, you are probably expecting a real example. I don't want to disappoint you, so here it is, directly from this site.

We have the following Rails 2 form, which needs to show/hide a loading spinner to indicate the execution of the AJAX request.

<% form_remote_tag :url => { :action => 'run' },
            :id => "tool-form",
            :update => { :success => "response", :failure => "error" },
            :loading => "$('#loading').toggle()", :complete => "$('#loading').toggle()" %>

Of course, there are several different ways to accomplish this task and if I would go back, I would probably use a different jQuery-oriented alternative, but in the past it was dead simple to add such callbacks using Rails so let's just upgrade the existing implementation to work with Rails 3.

First, remove all Rails 2 stuff and add the Rails 3 :remote option.

<% form_tag url_for(:action => "run"), :id => "tool-form", :remote => true do %>

Then, bind the function to toggle the spinner visibility to the appropriate AJAX events. Also, on success replace the content of #response with the response data.

jQuery(function($) {
  // create a convenient toggleLoading function
  var toggleLoading = function() { $("#loading").toggle() };

  $("#tool-form")
    .bind("ajax:loading",  toggleLoading)
    .bind("ajax:complete", toggleLoading)
    .bind("ajax:success", function(event, data, status, xhr) {
      $("#response").html(data);
    });
});

Remember

The upgrade to Rails 3 can be a bit more complicated if you heavily relied on Rails JavaScript generators in the past, but the benefits of switching to the Unobtrusive JavaScript patterns is definitely worth the effort.

Rails 3 forces developers to have a deeper JavaScript knowledge than in the past. Don't underestimate the importance of this task, it's an excellent chance to learn something more about the most important programming language today.

I hope this article will help you migrating upgrading your application to Rails 3.