Disable Rails 3 Remote Links After Click (using jQuery)

23 December 2010

I've frequently come across the need to disable remote AJAX links in Rails 3 after the first time they are clicked.

The typical scenario is that we have a link that fetches content from the server and loads it into the page. The problem is that after the first time the user clicks it, and the content is loaded into the page, we don't want to keep fetching the same content from the server and re-loading it into the page if the user clicks it again.

The ideal solution

Ideally, we could easily just unbind the `click.rails` event binding to disable the links after we click them. In fact, that's why I added the `.rails` namespace to the event bindings in rails.js in the first place.

Unfortunately, jQuery is not able to unbind (aka `.die()`) `.live()` bindings for specific elements in this way. See the Problem with .die() section in Exploring jQuery .live() and .die().

The good news is that I've rewritten the `.die()` method in jQuery to go along with the namespacing change to rails.js, so that we can do this. And it looks like the jQuery core team is planning to pull my rewrite into jQuery 1.5 (see discussion).

But until then we'll have to make do with...

The real solution

The best solution is to simply override the click event with something else. I like to turn the link into a toggle that then allows the user to show and hide the content.

Here's how we make that happen. Let's assume our JavaScript is set up similarly to the code in Rails 3 Remote Links and Forms: A Definitive Guide.

First, let's add a function to our jQuery arsenal, called .turnRemoteToToggle. You can put this in your app's master.js:

jQuery(function ($) {
    turnRemoteToToggle: function(target,altText){
        var $el = $(this),
            altText = altText || "Hide";

            $el.data('expanded', false);
            target.slideUp(); // or target.hide() or other effect
            $el.data('expanded', true);
            target.slideDown(); // or target.show() or whatever

Notice the lines that set $el.data('expanded') to true/false. That allows us to easily get the state of the toggle in our other JS functions. Of course, we could also just check to see if the target element is visible with jQuery. But then what happens if we click "Hide" and then check if it's visible while it's still sliding up (or fading or whatever effect we chose). We'll get a false positive simply because it's still visible for another half-second.

Now the JavaScript that handles the response of the jQuery AJAX request will include something like this:

The event hooks in jquery-ujs have been changed since this article was written. Thanks to Alex in the comments below for reminding me to update this. Changed ajax:loading to ajax:beforeSend in the code below.

    .live("ajax:beforeSend", function(){
      var $clicked = $(this);
      $clicked.data('origText', $clicked.text());
    .live("ajax:success", function(evt, data, status, xhr){
      var $clicked = $(this),
          $target = $( this.getAttribute("data-target") );

      $target.append( xhr.responseText );
      $clicked.turnRemoteToToggle( $target, "Return home" );

You'll also notice from the above function that we're depending on one of the link's attributes called data-target. This is just an arbitrary convention I've adopted, taking advantage of HTML5's valid data- attributes. We could also hardcode the target in our JS if we wanted. To work with the above JS, our view would look something like this:

<div id="rock-faces-off"></div>

<%= link_to "Go to Rock & Roll Hall of Fame",
       { :action => 'get_pick_of_destiny' }, 
      :id => 'jack-black', 
      :remote => true, 
      'data-target' => '#rock-faces-off' %>

About the author:

Steve Schwartz // Owner of Alfa Jango, CTO of Genomenon, co-founder of Carcode (acquired by Edmunds.com in 2014), engineer, developer, open-source enthusiast, guitarist, and racecar driverist.

Comments are loading...