Rails jQuery UJS: Now Interactive

22 April 2011

The Rails jQuery UJS adapter (aka jquery-ujs) has undergone some major renovations over the past few weeks. This particular change is one I'm very excited about, as I've been wanting to do this for a while. Without further ado, jquery-ujs is now InteractiveTM.

What do I mean by "interactive"? Well, we can now plug into every facet of jquery-ujs, binding to custom events, and even customizing internal functions, without hacking or monkey-patching the rails.js file itself.

We'll start with a brief explanation of the change and then dive into a few examples.

Feel free to bypass my blabbering and check out the the commit.

Closure style

Previously, rails.js was one big closure.

Before:

(function($) {
  ...
  function fire(obj, name, data) {
    var event = $.Event(name);
    obj.trigger(event, data);
    return event.result !== false;
  }
  ...
})( jQuery );

// cannot access fire() function here

This closure design had many advantages. However, the biggest advantage, in this case, happened to be the biggest disadvantage as well: everything inside was self-contained and inaccessible by the outside world.

So, what if we like most of what jquery-ujs does for us, but just want to modify one little function? Before, we'd have to open rails.js in our editor and start hacking away. But no more!

Object literal style

A relatively simple refactor using the object literal design pattern now allows us total control, by placing all internal functions into the externally-accessible $.rails object.

After:

(function($) {
  var rails;
  $.rails = rails = {
    ...
    fire: function(obj, name, data) {
      var event = $.Event(name);
      obj.trigger(event, data);
      return event.result !== false;
    }
    ...
  };
  ...
})( jQuery );

// Can now access $.rails.fire() here

Now, inside rails.js, we simply change any `fire()` function calls to `rails.fire()`. And outside of rails.js, we can similarly call `$.rails.fire()`.

Furthermore, we could even redefine the `fire()` function with our own: `$.rails.fire = someNewFunction();`.

In the excerpted code from rails.js above, the "before" looks simpler, but the "after" actually does a nice job of keeping things clearer and better organized when there are more functions.

Some examples

So, how might we actually use this now in a production app? Let's look at a couple examples.

Direct access

For starters, let's say that we want for a particular form to be submitted the same as `:remote => true` forms, but for whatever reason, this form does not have the remote attribute. We can now take advantage of the functionality provided by rails.js directly. In our application.js, we might have:

$('#my-form').live('submit', function(e) {
  $.rails.handleRemote( $(this) );
  e.preventDefault();
});

And now, rails.js will set up our AJAX request, serialize our form, etc., just as if it were a remote form. Of course, this example is a bit contrived, but hopefully it illustrates the potential for being able to access rails.js functions directly.

Redefining jquery-ujs functions

And of course, as mentioned before, there's also the ability to redefine functions. For example, in our application.js, we could add:

$.rails.fire = function(obj, name, data) {
  var event = $.Event(name);

  // Custom code:
  // e.g. "Fire ajax:before for my-form!"
  console.log("Fire " + name + " for " + obj.attr('id') + "!");

  obj.trigger(event, data);
  return event.result !== false;
};

And now we have our own custom `fire()` function which will now be used internally by jquery-ujs without having to open up and modify rails.js.

On a side-note, the `fire()` function in rails.js is probably one of my favorite little functions that I've actually used in several other projects, including the jQuery EasyTabs plugin. But we'll take a closer look at this function another time.

Redefining selectors

And as an added bonus, we also moved the CSS selectors, to which jquery-ujs binds all its functionality, into the `$.rails` object. How could this be useful?

Let's say we want the ability to kill all jquery-ujs form bindings for a particular form (maybe after it's been submitted once already). In rails.js we have:

$.rails = rails = {
  ...
  // Form elements bound by jquery-ujs
  formSubmitSelector: 'form',
  ...
}

Now in our application.js, we could do something similar to the following:

$.rails.formSubmitSelector = 'form:not([data-skip-remote])';

$('#my-form').live('ajax:success', function() {
  $(this).attr('data-skip-remote', 'true');
});

W00t

Well I hope this was helpful. One of my goals with these changes was to spur the development of additional plugins and gems that build off of and extend the great out-of-the-box functionality that rails.js provides.

As another way to help maintain third-party plugins that extend jquery-ujs, I also added a new Changelog. This will hopefully make it easier to keep track of notable changes to the "api".

I'd also like to send a big thank-you to Neeraj Singh, who has been very helpful keeping me sane and in getting changes pulled into jquery-ujs.

Stay tuned for more on recent developments in Rails jQuery UJS. In the meantime, I encourage you to check out the source.

If you liked this, check out our Definitive Guide to Rails 3 Remote Links and Forms Part 1 and Part 2.

About the author:

Steve Schwartz // Owner of Alfa Jango Web-based Software, creator of RateMyStudentRental & LeadNuke, engineer, hacker, rubyist, guitarist, aspiring racecar driverist.



Comments are loading...