Porting Lowpro to jQuery, Now With More Awesome

I’ve been porting a major Rails application (Groupon) from prototype to jQuery lately. The approach I ended up taking was to write some jQuery extensions that would allow all of our existing code to run on top of jQuery with no changes. So, here is a completely Prototype syntax compatible version of Lowpro.

However, this version of Lowpro has a bit more awesomeness with some help from jQuery.live. Check out the demo page for an example of what I mean. Here’s a breakdown of everything:

A behavior is just a class (remember Prototype’s Class.create?) with some extra magic. Here’s an example of a behavior:

var SomeBehavior = Behavior.create({
  initialize: function() {
  },

  onclick: function() {
  }
});

And here’s how you would attach the behavior to a set of elements:

Event.addBehavior({
  'p': SomeBehavior
});

What that translates to at runtime is something like this:

$(document).ready(function() {
  $('p').each(function(index, element) {
    new SomeBehavior(element);
  });
});

In actuality, there is a bit more syntactic sugar provided by adding a $.fn.attach method, so the code actually does this:

$.fn.extend({
attach: function() {
  var args = $.makeArray(arguments), behavior = args.shift();

  return this.each(function() {
    attachBehavior(this, behavior, args);
  });
},

var attachBehavior = function(el, behavior, args) {
  if(behavior.attach){
    instance = behavior.attach(el, args[0].selector);
    if (!behavior.instances) behavior.instances = [];
    behavior.instances.push(instance);
    return instance;
  }

  else
    behavior.call(el);

};

The attachBehavior function does two things. 1) it checks if behavior.attach exists. If so, then the behavior passed in is an actual behavior class, so we call the attach method and push the instance into an array. 2) otherwise, we assume the behavior is a function so we just call it.

So what does behavior.attach do? Simply this:

Behavior = {
  create: function() { [omitted] },
  attach : function(element, selector) {
    return new this(element, selector, Array.prototype.slice.call(arguments, 2));
  }
}

So attach instantiates the specified behavior and returns the instance. Cool.

Now, Prototype’s Lowpro had a setting that automatically re-applied events after an Ajax request to handle re-setting up elements that were newly added to the DOM. That was done like this:

Event.addBehavior.reassignAfterAjax = true;

This has two issues. 1) It works by wrapping the onComplete callback on Prototype’s Ajax.Request, so elements added outside of Ajax requests still won’t get their behaviors re-bound. 2) It is overkill to re-bind all behaviors after each Ajax call when really there will only be one or two behaviors applied to any given element.

So, how can we do this better? A: With jQuery.live

The way we will utilize jQuery.live is when a behavior is attached to a selector, we look for any events that the behavior defines (methods that start with “on” – onclick, onkeyup, etc) and we attach jQuery.live bindings to the selector. Then when the live event is triggered, we check the behavior array to see if the element is already bound to that behavior. If it is, we do nothing, if it’s not we bind the element to the behavior and call the related event method. To do this, we are going to add to the $.fn.attach method:

$.fn.extend({
attach: function() {
  var args = $.makeArray(arguments), behavior = args.shift();

  // loop through the methods in behavior
  for (var member in behavior.prototype) {
    // is this method an event method?
    if (member.match(/^on(.+)/) && typeof behavior.prototype[member] == 'function') {
      // bind a live event to this event
      $(this.selector).live(RegExp.$1, function(event) {
        // is this element already bound to the behavior?
        // if yes do nothing, otherwise, let's bind it now
        if($(this).attached(behavior).length == 0){
          event.preventDefault();
          // remove the .live binding for this element
          $(this).die(event.type);
          //instantiate the behavior
          var instance = new behavior(this);
          // call the event method on the behavior
          // the behavior will handle this automatically from now on
          // but since the behavior just got bound, we need to do this manually
          instance['on' + event.type].call(instance, $(this));
        }
      })
    }
  }
  return this.each(function() {
    attachBehavior(this, behavior, args);
  });
}

So with a little bit of jQuery.live trickery, we only need to bind the behavior once on dom ready, and any matched elements will get bound at any time. Awesome.

Check it out on Github

Post a Comment

Your email is never shared. Required fields are marked *

*
*