
Duplicate link_to_remote With Rails 3 and UJS
July 19, 2010
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_to
s 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.
■