Duplicate link_to_remote With Rails 3 and UJS

July 19, 2010 — Code

In Railscast #205 Ryan Bates shows how to use UJS to update an element using AJAX. However, with his technique the server returns a JavaScript response: code which inserts a string into an element on the page. The problem with this approach is that the server needs to know where to insert its response (DOM ID of the target element). Most of my Rails 2 applications were designed so that each page is a “client” which receives a plain HTML response from the server and inserts the body of that response into the appropriate element on the page. This means the same server action (URL) can be used to generate content that gets inserted into different pages on the site. Not only does this make server actions simpler and more general, it makes their output easier to test.

Turning HTML into a JavaScript-escaped string on the server just doesn’t feel right.

How To Do It

Rails 3 dictates that we replace things like this:


<%= link_to_remote "more", :url => {...} %>

with things like this:


<%= link_to "more", {...}, :remote => true %>

With good old link_to_remote we could pass an :update option to achieve the system described above, in which the page receives a generic response and updates the specified element with it. But link_to doesn’t accept an :update option because with Rails 3 and UJS we want to avoid putting JavaScript in our <a> tag attributes. In Rails 3 link_to with remote="true" renders something like this:


<a href="..." data-remote="true">more...</a>

Then the Rails UJS drivers (currently available for Prototype and jQuery) look for links and forms with a data-remote="true" attribute and AJAX-ify them. So how do we do this? If we don’t write the code that makes the AJAX request, how can we access the response? The answer is: events.

The UJS drivers trigger six events on every AJAX call:

  • ajax:before
  • ajax:loading
  • ajax:success
  • ajax:failure
  • ajax:complete
  • ajax:after

You should recognize these event names from the old link_to_remote method options, and we simply need to listen for them on our AJAX-triggering elements.

Example

Let’s say we have a page that lists the ten most popular products in our database. At the end is a link to load all the remaining products via AJAX:


<ul id="products-list">
  <li>...</li>
  <li>...</li>
  <li>...</li>
  ...
</ul>
<%= link_to "more...", "/products/all",
  :id => "load-more", :remote => true %>

The server returns plain HTML and we use the ajax:x events triggered by the Rails UJS drivers (here jQuery) to grab the server’s response and insert it into the right element on the page:


<%= javascript_tag do %>
  $("#load-more").bind("ajax:complete", function(et, e){
    $("#products-list").html(e.responseText); // insert content
  });
<% end %>

If desired, we can also use the ajax:loading event to change the link text (or show a spinner):


<%= javascript_tag do %>
  $("#load-more").bind("ajax:loading", function(et, e){
    $(this).html("Loading..."); // swap link text
  });
<% end %>

Unexpected Token Error

In implementing the above solution you may receive a JavaScript error, something like this:


Uncaught SyntaxError: Unexpected token

Nathaniel Bibler explains:

Here’s the problem: if you’re building a RESTful interface in Rails, then you’ve probably got respond_tos setup for HTML and JS. So, in this case, the browser load hits /products and loads the original product list. It renders a link to AJAX load the additional products. Let’s say that action is also to /products (but for JS). So, as you’ve described in your tutorial, you’d like to just render HTML back to the client for the JS request. That’s fine, except that by default jQuery will expect that it’s executable JavaScript, since it’s making a JavaScript request (this is defined by the jQuery.ajax(:dataType)). So it’ll get the HTML string back and attempt to interpret it as javascript and blow up.

To fix that, you’d think you can just set the dataType to 'html'. Well, yes, and no. That fixes it from jQuery’s perspective. It will no longer try to execute it and you’ve now gotten past the Unexpected token issue. But, in doing so, jQuery will now make the AJAX request with an Accept: text/html header. So, Rails will now think you’re a web browser requesting the standard HTML page and will not render your JS view. So, now you’re sending back the page with full layout. To sum up, jQuery and Rails are not quite on the same page if you’re making a “javascript” request but returning “html.” It breaks things.

By the way, you can dictate the dataType to use by setting a "data-type" attribute on your remote link:


<%= link_to 'foo', '/foo', :remote => true, 'data-type' => 'html' %>

which renders:


<a href="/foo" data-remote="true" data-type="html">foo</a>

If, however, you want this functionality, I would suggest using the "text" data-type. Basically what would happen here is that jQuery will not execute the returned content. So, that fixes your first issue. Rails will still, however, attempt to render a TEXT response, since jQuery will post an Accept header with "text/plain" in it, first. As long as you don’t have a respond_to defined for "txt" (or some other "text/plain"), then Rails will continue down the Accept header string to locate an acceptable response. jQuery is smart enough to also contain an application/javascript string in there, so, eventually, Rails will decide that a JavaScript response is acceptable, and render the JS response you’re expecting. It’s very slightly hacky, but it works.


comments powered by Disqus