Rails, Ajax and jQuery

The more ajaxified application, the more fun it is to use. But it is also more painful do develop. What is written below is my approach to pairing Rails and Ajax. It’s a mix of tips I found over the net on blogs and forums. I use jQuery for JavaScript, but I don’t use jRails or any JS/Ajax helper methods provided by Rails. Note that all Javascript/HTML code presented here can be used even if you dont use Rails or Ruby as your web development platform. Let’s begin.

Rails is RESTful

Thanks to Rails’ RESTfulness the only thing to take care of server side is setting proper response in controllers’ actons.

class PostsController < ActionController::Base
  def index
    @posts = Post.find :all
    respond_to do |format|
      format.html
      format.xml { render :xml => @posts.to_xml }
    end
  end
end

Rails decides which format block to call basing on routes defined in routes.rb file (map.connect ‘:controller/:action/:id.:format’) and accept headers sent with request by the client.

In most cases we want Ajax requests to trigger format.xml blocks in our controllers’ actions, so we need to set proper accept headers. Let’s do it just once with application-wide setting.

// All ajax requests will trigger the format.xml block
// of +respond_to do |format|+ declarations
$.ajaxSetup({
  'beforeSend': function(xhr) {xhr.setRequestHeader("Accept", "text/xml")}
});

Browsers’ quirks

There is something worth noting here, a problem I had once with IE and Safari. The code above may work differently in various browsers. Browser set text/html accept header by default. Here IE and Safari will append text/xml to it so you’ll get something like ‘text/html; text/xml’, while Firefox will replace text/html with text/xml and you’ll get ‘text/xml’ only. This is very important because Rails will take the first format it detects in accept header and trigger respective block in controller’s action, which will be html for IE and Safari. Here’s a fix for this that shifts application/xml (if it is present) to the beggining of accept headers array.

class ApplicationController &lt; ActionController::Base
  before_filter :correct_safari_and_ie_accept_headers
 
  def correct_safari_and_ie_accept_headers
    request.accepts.sort!{ |x, y| y.to_s == 'application/xml' ? 1 : -1 } if request.xhr?
  end
end

Ajaxify your links

Here’s a quick way to ajaxify your existing links. Add this JavaScript to your application.js file.

jQuery(document).ready(function() {
  // All A tags with class 'get', 'post', 'put' or 'delete' will perform an ajax call
  jQuery('a.get').livequery('click', function() {
    var link = jQuery(this);
    $.get(link.attr('href'), function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.post').livequery('click', function() {
    var link = jQuery(this);
    $.post(jQuery(this).attr('href'), "_method=post", function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.put').livequery('click', function() {
    var link = jQuery(this);
    $.post(jQuery(this).attr('href'), "_method=put", function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.delete').livequery('click', function() {
    var link = jQuery(this);
    $.post(jQuery(this).attr('href'), "_method=delete", function(data) {
      if (link.attr('ajaxtarget'))
        jQuery(link.attr('ajaxtarget')).html(data);
    });
    return false;
  }).attr("rel", "nofollow");
 
  jQuery('a.get, a.post, a.put, a.delete').removeAttr('onclick');
});

Just add a CSS class .get, .post, .delete, or .put to a link to make turn it into an ajax-link. I recommend you use LiveQuery plugin which will automatically bind click events to new links that appear on the page (loaded with Ajax call for-example). You can optionally set ajaxtarget attibute of the link. It expects a selector of a container in which you want to place the response.

link_to 'my cool article', article_path(@article), :class => 'get', :ajaxtarget => '#article_container'

Ajaxify your forms

For this you’d need jQuery Form Plugin.

  jQuery('form.ajax').livequery('submit', function() {
    jQuery(this).ajaxSubmit();
    return false;
  });

Now all your forms that have “ajax” class will be submitted via Ajax.

<form class="ajax">
  ...
</form>

CSRF and authenticity token

Rails has built-in protection from cross-site request forgery attacks. It relies on an authenticity token which Rails look for when dealing with POST, PUT or DELETE requests, so this token needs to be sent by the browser together with the request. The token is automatically added as a hidden field to any form you create with form_for method, it is also attached to links that have :method param set to :post, :put or :delete. In fact the token is added dynamically by Javascript code placed in link’s onclick attribute. However in one of code snippets above we stripped that onclick attribute from links to prevent the page reload after we click the link. Now we need to attack that token ourselves. First we will alter our application layout:

<head>
  <% if protect_against_forgery? %>
    <script type='text/javascript'>
    //<![CDATA[
      window._auth_token_name = "#{request_forgery_protection_token}";
      window._auth_token = "#{form_authenticity_token}";
    //]]>
    </script>
  <% end %>
</head>

Now we need to ensure that the token is sent together with ajax requests.

jQuery(document).ready(function() {
  // All non-GET requests will add the authenticity token
  // if not already present in the data packet
  jQuery("body").bind("ajaxSend", function(elm, xhr, s) {
    if (s.type == "GET") return;
    if (s.data && s.data.match(new RegExp("\\b" + window._auth_token_name + "="))) return;
    if (s.data) {
      s.data = s.data + "&";
    } else {
      s.data = "";
      // if there was no data, $ didn't set the content-type
      xhr.setRequestHeader("Content-Type", s.contentType);
    }
    s.data = s.data + encodeURIComponent(window._auth_token_name)
                    + "=" + encodeURIComponent(window._auth_token);
  });
});

We’re done, we have our ajax requests protected from CSRF attacks.

Modifing page after Ajax calls

Standard way to do page modification after Ajax call is to use Javascript code that inserts content returned by the call somewhere on the page. The other method is to put the modifying code in views that are returned by the server and just execute it in the browser. For this I’d recommend another jQuery plugin – Taconite. As the author says: “The jQuery Taconite Plugin allows you to easily make multiple DOM updates using the results of a single AJAX call. It processes an XML command document that contain instructions for updating the DOM”. Thanks to this you can for example easily use flash messages in your Ajax views.

Let this be a part of your usual layout:

<div id="flash_notice" class="flash"<%= ' style="display: none"' unless flash[:notice] %>><%= flash[:notice]  %></div>

Now let this be your taconite layout you’d use when returning views for Ajax requests:

<taconite>
  <hide select="#flash_notice" />
  <% if flash[:notice] %>
    <replaceContent select="#flash_notice">
      <%= flash[:notice] %>
    </replaceContent>
    <fadeIn select="#flash_notice" arg1="slow" />
  <% end %>
  <%= yield %>
</taconite>

This will display flash notice messages with fade-in effect after Ajax requests. Similarly you can update other elements of the page.

What’s in your toolbox?

I would love to hear from you on how you deal with Ajax in your web applications. What libraries/plugins do you use?

  • http://gaskell.org Andy Gaskell

    What’s the reasoning behind not using jRails?

  • Michał Szajbe

    I never used jRails, because I always had some JS code from other (non-Rails) apps that could be easily plugged in. Similarly all the JS/HTML code above can be used in any application in any language/framework.

    Maybe jRails would be helpful somewhere, I just don’t know.

  • http://henrik.nyh.se Henrik N

    It’s weird how you mix $ and jQuery. I suggest wrapping it all in (function($) { // your code })(jQuery); and just using $. If you need to worry about conflicting libraries at all.

    I believe edge Rails is removing support for accept headers by default, so better to append .xml to your URLs.

    I suggest this.href over jQuery(this).attr('href').

    Not a fan of the nonstandard ajaxtarget attribute. I would keep that information in my JS files, outside the markup. If you really want it in the markup, perhaps add it as a fragment on the URL? /foo.xml##my_id or something.

  • http://railsonedge.blogspot.com/ Ralph

    Great write-up…I have used some of these techniques…taconite looks pretty cool, I would like to try it out.

    Andy – I don’t use JRails either…to me, JQuery is about taking the javascript out of your markup…JRails is not unobtrusive JS.

  • http://charliepark.org/ Charlie Park

    Thanks for this writeup. I’m just getting started with jQuery and Rails, so I really appreciate this.

    Couldn’t that “// All A tags with class ‘get’, ‘post’, ‘put’ or ‘delete’ will perform an ajax call” javascript get DRYed up a bit? (I honestly don’t know, but suspect it could be.)

  • http://www.rubyrailways.com Peter Szinek

    Great stuff! Just wanted to cover the same myself, but I doubt I could add much to the above.

  • http://derenci.us marcus derencius

    I use lowpro (there is a jquery version) to wrap and organize livequery.

    Why not just use js.erb views instead of toconite?

  • Michał Szajbe

    @Henrik
    Thanks for valuable feedback!

    @Charlie
    It probably could be DRYed up. I’ve seen someone using Array.foreach() for this, but I think it’s not going to work in all browsers. I need to take a closer look on this.

    @marcus
    I don’t know if I correctly understand your question. What would be the difference? I use taconite because that’s where I want to put page-modifying code instead putting in the page from which ajax request was made.

  • http://joeloliveira.com joel

    fantastic post! all of this will come in mighty handy.

  • http://andyjeffries.co.uk Andy Jeffries

    @ Michał: I don’t know if Marcus meant RJS. If he did, that might explain the reason for your confusion over his comment. You’d achieve exactly the same thing with RJS. Personally I don’t see the advantage of Taconite over Rails’ more ‘native’ RJS, but maybe I’m missing something.

  • http://www.rubyrailways.com Peter Szinek

    @ Michał: How about forgery protection? I had t turn it off for POST stuff (e.g. destroy) otherwise Rails was complaining.

  • Michał Szajbe

    @Peter

    I forgot about that issue. Thanks for reminding me about it. I added the paragraph ‘CSRF and authenticity token’ to the article.

  • http://www.tutornearby.com alex tretyakov

    Thanks for good and informative article.
    But I have question – why you change rel attributes to “nofollow”? I dont think google bot will proceed JS..

  • http://www.rubyrailways.com Peter Szinek

    Thanks man, great stuff!

  • Pingback: Rails 2.2.2, Ajax and respond_to at code tunes

  • Pingback: //DEVGURU » Blog Archive » Rails 2.2.2, Ajax and respond_to

  • http://calicowebdev.com steve

    Thanks for this post. Jeez. Browser incompatibility H*ll.

    I’d suggest that for the before_filter in ApplicationController you use:

      def correct_safari_and_ie_accept_headers
        ajax_request_types = ['text/javascript', 'application/json', 'text/xml']
        request.accepts.sort!{ |x, y| ajax_request_types.include?(y.to_s) ? 1 : -1 } if request.xhr?
      end

    This sorts the three most commonly used formats to the top of the accepts list, not just xml.

  • mick

    any chance u could put up a code download of the work in this post???
    :-)

  • GR

    I just wanted to chime in and thank you for this post and for introducing me to Taconite. I am using it on a Rails project now and it has been working out really well (alongside respond_to and *.xml.builder XML Rails views to generate the Taconite code).

    There are so many different methods and tools for implementing AJAX on a site it can be a bit bewildering for the AJAX newbie. Nice to see you not only gave us a suggested approach, but a bunch of helpful sample code as well for ajax form and link submission.

    I’ve subscribed to your feed. Keep up the good work.

  • Michał Szajbe

    @mick I don’t see a reason, I believe all the code you can copy-paste from this post. Sorry.

  • Peter

    I think jQUery 1.3 provides the same functionality as livequery in the new live event binding, found here http://docs.jquery.com/Events/live

    I’m not too familiar with the details of livequery. Is there any reason to continue using it over live?

    • Michał Szajbe

      That’s true. You don’t need additional livequery plugin with jQuery 1.3.

  • Vijay

    Thanks for the post and its a great one! I have been on ways to use Ajax for querying data from Mysql database. If you could provide any insight it will be of great help! Thank you!

    • Anonymous

      Vijay,
      It really doesn’t matter what your backend is. When you make AJAX requests to your application, you simply access controllers and it’s their role to fetch actual data from database and respond. However you’d probably want the response to be in other format than usual HTML. I’d suggest JSON instead. But again, it’s controller’s role to format the data.

      Also note that this post is pretty old and there are now better methods to use AJAX in Rails. Actually there is a built-in unobtrusive AJAX support in Rails 3.

  • Guest

    Vijay,
    It really doesn’t matter what your backend is. When you make AJAX requests to your application, you simply access controllers and it’s their role to fetch actual data from database and respond. However you’d probably want the response to be in other format than usual HTML. I’d suggest JSON instead. But again, it’s controller’s role to format the data.

    Also note that this post is pretty old and there are now better methods to use AJAX in Rails. Actually there is a built-in unobtrusive AJAX support in Rails 3.