Rails 3 Remote Links and Forms: A Definitive Guide

28 October 2010

Also see Rails 3 Remote Links and Forms Part 2: Data-type (with jQuery).

Spoiler Alert: If you like magic, stop reading. The Rails 3 UJS driver (rails.js), which powers the remote links, forms, and inputs, is not very magical when you know how it works.

This article uses the jQuery UJS driver, though the Prototype UJS driver does the same thing. UJS, by the way, stands for "Unobtrusive JavaScript".

If you have any experience with jQuery, take a few minutes to check out the rails.js source. It's pretty straight forward, and only 147 lines as of this writing. I'll be here waiting to answer your questions when you get back.


What rails.js does

  1. It finds remote links, forms, and inputs, and overrides their click events to submit to the server via AJAX.
  2. It triggers six javascript events to which you can bind callbacks to work with and manipulate the AJAX response.
  3. It handles the AJAX response from the server

What rails.js does NOT do

Notice that last bit is struck-through. This seems to be the greatest source of confusion when starting with the new Rails 3 remote functionality. Thanks to habits engrained by Rails 2's link_to_remote and remote_form_for, we expect that Rails 3 would also handle the AJAX response for our remote links and forms. But it doesn't; it leaves that for you.

Rails will take your luggage up to your room, but it won't unpack your bags for you. This is by design; why would you want the bellhop going through your stuff? Also, the actual handling of the data is largely unique to each element, and depends on the data you're working with in each case. So Rails leaves that part up to you. We'll come back to this.

The role of HTML5

You've likely heard that Rails 3 uses HTML5. That's true, but the actual role that HTML5 plays may be simpler than you suspect.

Remember the first thing rails.js does? Before it can override the click and submit events of all the remote elements, it first needs to find all the remote elements.

So, we need a way to designate our remote elements. In ancient times, we might have added class="remote" to our remote elements. But classes are for styling. Isn't there a way to differentiate remote links that doesn't pollute our CSS classes?

With HTML5, there is. Part of the HTML5 spec is that you can now add any arbitrary tag to an HTML element, as long as it starts with "data-". So, this is now valid HTML5 markup:

<a href="stuff.hml" data-rocketsocks="whateva">Blast off!</a>

Rails 3 takes advantage of this new valid markup, by turning all links and forms with :remote => true into HTML elements that have the tag data-remote=true.

And now the rails.js finds all remote links with the selectors:

$('form[data-remote]')
$('a[data-remote],input[data-remote]')

... and overrides the submit and click actions, respectively, with an ajax request to the forms' action links' href properties. Then it does the same thing wirh remote forms and inputs, using the form's action.

So, that's it, HTML5 just provides a convenient, semantic, way for us to designate and select which elements to hijack.

Handling the AJAX response

Okay, great, how the hell do we actually do something with the AJAX response? Thankfully, when rails.js sends your requests remotely, it also triggers six custom events along the way, passing the corresponding data/response to each event. You can bind your own handler functions to any of these events.

These six events are (in order):

ajax:before   // fires before the request starts, provided a URL was provided in href or action
ajax:loading  // fires after the AJAX request is created, but before it's sent
ajax:success  // fires after a response is received, provided the response was a 200 or 300 HTTP status code
ajax:failure  // fires after a response is received, if the response has a 400 or 500 HTTP status code
ajax:complete // fires after ajax:success or ajax:failure
ajax:after    // fires after the request events are finished

Update: Since this article was written, the Rails team has made some changes to the jQuery UJS driver. In the most recent versions of rails.js, there are now only 4 callback events:

ajax:beforeSend // equivalent to ajax:loading in earlier versions
ajax:success
ajax:complete
ajax:error // equivalent to ajax:failure in earlier versions

So in your page, you would bind to these events with a function like:

$('.button-link').bind('ajax:success', function(){
  alert("Success!");
});

You'll notice that all of the Rails JavaScript functionality is facilitated by binding some function to some entity/event.

Binding is good, because it is unobtrusive. The JavaScript functionality and the HTML markup each exist as wholly separate units, which are then bound together via JavaScript. If the user has no JavaScript, then the JavaScript that binds the JS functions to the HTML entities is never executed, and the user is left with only clean, valid HTML.

Putting it all together

Enough explanation, let's create a remote form that loads some content into the page. We'll provide the user with instant feedback, fully handle any errors, and reset the form so they can do it again. Armed with the knowledge above, hopefully none of the following will seem like magic.

The following assumes we have a Comment model in our Rails app, which contains a "content" text column in the database. We will allow a user to create a comment, by submitting the form remotely, and then insert the comment into the page.

You'll notice we're requesting an HTML response directly, but then returning errors in JSON. We're also overriding much of the magic provided by respond_with. This is all to illustrate how much control we have over the request/response.

See the follow-up post describing how to request a JS (or XML or JSON or text or whatever) response, and with a more production-appropriate example.

View

Controller

Obviously, we would have a _show.html.erb partial view, which would be our template for displaying the comment. Hopefully you know how to do that.

At this point, we have a form that remotely submits to our server, and our server responds with our view partial, ready to be inserted into the page. Just one thing... it's not actually being inserted into the page.

JavaScript

It's time to bind some handler functions to those triggered "ajax" events. So, in the page that has our form, we'll want to include this javascript (probably in the <head> section or in a separate js file).

Update: The following has been updated to bind to the callbacks available in the most recent versions of rails.js.

$(document).ready(function(){

  $('#create_comment_form')
    .bind("ajax:beforeSend", function(evt, xhr, settings){
      var $submitButton = $(this).find('input[name="commit"]');

      // Update the text of the submit button to let the user know stuff is happening.
      // But first, store the original text of the submit button, so it can be restored when the request is finished.
      $submitButton.data( 'origText', $(this).text() );
      $submitButton.text( "Submitting..." );

    })
    .bind("ajax:success", function(evt, data, status, xhr){
      var $form = $(this);

      // Reset fields and any validation errors, so form can be used again, but leave hidden_field values intact.
      $form.find('textarea,input[type="text"],input[type="file"]').val("");
      $form.find('div.validation-error').empty();

      // Insert response partial into page below the form.
      $('#comments').append(xhr.responseText);

    })
    .bind('ajax:complete', function(evt, xhr, status){
      var $submitButton = $(this).find('input[name="commit"]');

      // Restore the original submit button text
      $submitButton.text( $(this).data('origText') );
    })
    .bind("ajax:error", function(evt, xhr, status, error){
      var $form = $(this),
          errors,
          errorText;

      try {
        // Populate errorText with the comment errors
        errors = $.parseJSON(xhr.responseText);
      } catch(err) {
        // If the responseText is not valid JSON (like if a 500 exception was thrown), populate errors with a generic error message.
        errors = {message: "Please reload the page and try again"};
      }

      // Build an unordered list from the list of errors
      errorText = "There were errors with the submission: \n<ul>";

      for ( error in errors ) {
        errorText += "<li>" + error + ': ' + errors[error] + "</li> ";
      }

      errorText += "</ul>";

      // Insert error list into form
      $form.find('div.validation-error').html(errorText);
    });

});

Each of these functions could easily be abstracted, so that they can be easily or automatically applied to all of our remote forms.

These bindings also work the same way with remote links, but I figured we'd use a remote form for this example, since it requires a couple extra steps that aren't immediately obvious (like clearing the form or populating validation errors).

Also, note that the data passed to your functions from the ajax events are not in the same order. We have evt, data, status, xhr for ajax:success, and evt, xhr, status, error for ajax:failure. This will get ya every time.

But wait, there's more

If you did your homework at the beginning of the article, and looked at the rails.js file, you would have noticed a couple additional details.

.live()

Instead of directly binding to the click events of remote links, forms, and inputs, rails.js actually uses .live(). Likewise, in the javascript above, you could replace .bind() with .live() to be a bit more versatile.

See Exploring jQuery .live() and .die() and The Difference Between jQuery’s .bind(), .live(), and .delegate() for more info.

Update: If you're going to use .live() to live-bind your AJAX handlers, be sure you're on the latest jQuery (> v1.4.4), because there are issues with v1.4.2 in IE.

Update: The .live() method has been deprecated, so these days instead use $(document).delegate() or .on().

:confirm => "Are you sure?"

You'll also notice that the rails.js file handles :confirm => "Are you sure?" as well (which will pop up a box that says "Are you sure?" when you click or submit something (you can also just pass in :confirm => true for the default message)). Just like the remote functionality, Rails 3 simply adds the HTML5-valid tag data-confirm=true to your HTML elements.

:disable_with => "Submitting..."

Rails 3 gives us another option called, :disable_with, that we can give to form input elements to disable and re-label them while a remote form is being submitted. This adds a data-disable-with tag to those inputs, to which rails.js can select and bind this functionality.

This could be used in place of our ajax:loading and ajax:complete bindings in the example above. But then I'd need to come up with something else for those bindings to do, to show how they work.

To see how to get remote links and forms working with js.erb (or any other format for that matter), check out Part 2 to this article.

Additional Resources

By some stroke of temporary sanity, I actually wrote down the links I came across when I first tackled Rails 3 remote functionality.

If you're itching for more information, check out these resources (and for crying out loud, go read the source code already!). In no particular order:

About the author:

Steve Schwartz // Owner of Alfa Jango Web-based Software, creator of RateMyStudentRental & LeadNuke, engineer, hacker, rubyist, guitarist, aspiring racecar driverist.



Comments are loading...