method_missing:
A Rubyist’s Beautiful Mistress
jQuery EasyTabs Plugin V2

Rails 3 Remote Links and Forms:
A Definitive Guide

October 28th, 2010 by Steve Schwartz

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

spacer

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.

<%= link_to "Get remote sauce", {:action => "sauce"}, :remote => true, :class => "button-link" %>
# => <a class="sauce" class="button-link" data-remote=true>Get remote sauce</a>

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

<%= form_for @comment, :remote => true, :html => { :'data-type' => 'html', :id => 'create_comment_form' } do |f| %>
  <%= f.text_area(:content) %>
  <div class="validation-error"></div>
  <%= f.submit %>
<% end %>

<div id="comments"></div>

Controller

respond_to :html, :xml, :json
...

def create
  @comment = Comment.new( params[:comment] )

  if @comment.save
    respond_with do |format|
      format.html do
        if request.xhr?
          render :partial => "comments/show", :locals => { :comment => @comment }, :layout => false, :status => :created
        else
          redirect_to @comment
        end
      end
    end
  else
    respond_with do |format|
      format.html do
        if request.xhr?
          render :json => @comment.errors, :status => :unprocessable_entity
        else
          render :action => :new, :status => :unprocessable_entity
        end
      end
    end
  end
end

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.

: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:

  • Rails link_to documentation
  • Rails 3 Beautiful Code (slideshare) – by Gregg Pollack
  • HTML5 data attributes – by John Resig
  • Unobtrusive JavaScript Helpers in Rails 3 – by Piotr Solnica
  • Duplicate link_to_remote With Rails 3 and UJS – by Alex Reisner
  • Unobtrusive JavaScript in Rails 3 – by Simon Carletti
  • Cleaner RESTful Controllers w/ respond_with – by Ryan Daigle
  • Rails HTTP Status Code to Symbol Mapping – by Cody Fauser
    (OK, this one is only tangentially related, but still helpful)
spacer

Posted: October 28th, 2010
Filed under: HTML5, Javascript, JQuery, Rails
Tagged with: AJAX, JQuery, link_to_remote, Rails, Rails 3, remote_form_for

You can follow any responses to this entry through the RSS 2.0 feed. You can leave a response, or trackback from your own site.

36 Responses to “Rails 3 Remote Links and Forms:
A Definitive Guide”

  1. spacer Juanma says:
    October 29, 2010 at 3:25 am

    Hello Steve.
    I have found your article very well written and very good for teaching how to make ajax applications with Rails 3.0
    But there is something that strikes me on my mind since I saw this in the first time, and it is that with format.js respond_to should give a response of javascript (you could even use a template file create.js.erb and it would be javascript), but in the sample the response is an html partial.
    I always have had the feeling of not doing the most correct code when doing things like this.
    I would like to know your opinion about this.
    Thank you.
    Juanma

    Reply
  2. spacer Steve Schwartz says:
    October 29, 2010 at 3:50 am

    Hey Juanma, that is a valid point, but I think it simply comes down to personal preference. The simple fact is you control the response and you control the response handling, so you can do whatever you want.

    In this particular example, think about what our js.erb would actually say. It would most likely be a one-line file with something like:

    $('#comments').append('escape_javscript(< %= render :partial => 'show', :locals => {:comment => @comment} %>');

    And then, in our view, we’d still need something like:

    $('#create_comment_form')
        .bind("ajax:success", function(evt, data, status, xhr){
            eval(xhr.responseText);
        });

    I dunno, I guess it just seems like a lot of unnecessary moving parts. But again, I think it simply comes down to personal preference. I’d be interested to hear what others’ thoughts are on this.

    Reply
  3. spacer solnic says:
    October 29, 2010 at 3:54 am

    @Juanma That’s a good point and I also find it as a common mistake in rails apps. What people usually do is to send an ajax call with accept header set to text/javascript even though they’re going to respond with an html. The correct way of doing this is to set accept header to text/html and in the action render correct template depending on whether it was an XHR request or not. For instance you could write:

    respond_to do |wants|
      wants.html do
        if request.xhr?
          render :partial => "items"
        else
          render
        end
      end
    end

    If rails UJS drivers generate ajax calls with accept headers set always to text/javascript and there’s no way of changing that then…well, they kinda suck again.

    Reply
  4. spacer Steve Schwartz says:
    October 29, 2010 at 4:13 am

    Also, to elaborate on the point I was making, think abou

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.