Synchronous Ajax in Rails 3

2011-08-17

Synchronous Ajax sounds like an oxymoron, but sometimes it is what you want. That is, you still want the Ajax requests to happen in the background, but you want to make sure they hit the server in the order the user clicked. (Note that many people use “synchronous ajax” to mean the network call blocks your main browser thread. That’s not what I’m talking about, and it is not sometimes what you want. :-) For example, suppose you have a multiple-choice question, and when the user clicks an answer, you record it using Ajax and also update the display to show the answer the user chose. You need to synchronize these requests, because whatever answer the user clicked last should override all the previous answers. If network strangeness causes your requests to hit the server out-of-order, the wrong answer is going to get recorded.

There is a nice jQuery plugin I’ve been using for these situations, called Ajax Manager. For something like my question/answer example, I’d use code like this:

$(function() {
  var answerManager = $.manageAjax.create({
    queue: 'clear',
    abortOld: true,
    maxRequests: 1
  });
  $('a.answer').click(function(event) {
    event.preventDefault();
    answerManager.add({
      url: $(this).attr('href'),
      method: 'POST'
    });
    return false;
  });
});

The trouble comes when I combine this with Rails 3. Suppose I’m creating my HTML with this Rails code (in HAML):

- @question.choices.each do |ch|
  = link_to question_answers_path(@question, :choice_id => ch.id), :method => :post, :remote => true do
    .choice= ch.text

One problem is that when Rails sees the :remote => true, it’s going to set its own jQuery listener on that link, which will interfere with my own click listener. So we take that off. But the real problem is saying :method => :post, because that

also

causes Rails to listen on the link, so it can turn the GET into a POST. Because Rails is going to send a POST request from its own Javascript code, it doesn’t matter that I’m returning false from my own click listener. The anchor tag will get canceled, but Rails’ Javascript will still submit a POST for me (and not via Ajax).

I could remove the :method => :post, but then link_to is going to complain because it thinks I want the :index route, not the :create route, and maybe that doesn’t exist. Arg! Besides, with this approach my code is misleading. The HAML looks like I want a GET link, but elsewhere I’m substituting a POST. We could just write the link by hand, without using link_to, but that doesn’t make the code any less misleading, and it’s just too far down the road of fighting your framework to make me comfortable.

The solution, which unties all these knots, is to use Rails’ Ajax callbacks to cancel Rails’ own Ajax submission and replace it with our own. With this approach, you keep :method => :post, :remote => true, so link_to is happy and your HAML makes sense. But in your Javascript, instead of listening for click, you listen for ajax:before:

$(function() {
  var answerManager = $.manageAjax.create({
    queue: 'clear',
    abortOld: true,
    maxRequests: 1
  });
  $('a.answer').bind('ajax:before', function() {
    answerManager.add({
      url: $(this).attr('href'),
      method: 'POST'
    });
    return false;
  });
});

Here we’re binding to this custom Rails-provided event, so instead of fighting with our framework, we’re cooperating with it. Note our event parameter is gone, but we don’t need it. We launch our own request via Ajax Manager, and then we return false to cancel the Rails request.

If you needed the xhr or settings parameters, you could bind to ajax:beforeSend instead. Either method is cancelable by returning false.

Here is some documentation on Rails’ Ajax integration, if you want to read more.

blog comments powered by Disqus Prev: Piping in Ruby with popen3 Next: RESTless Doubts