Yehuda Katz is a member of the Ruby on Rails core team, and lead developer of the Merb project. He is a member of the jQuery Core Team, and a core contributor to DataMapper. He contributes to many open source projects, like Rubinius and Johnson, and works on some he created himself, like Thor.

@garybernhardt @steveklabnik I guess I have to ask… is cls a troll project?

Archive for April, 2009

Evented Programming With jQuery

Over the past several years, I’ve been actively using jQuery for a variety of things. Early on, I shared the frustration that people had around using jQuery for more substantial projects. Starting with version 1.2 and continuing with version 1.3, however, jQuery provides a powerful evented model that can be used to build up fairly complex applications using evented, as opposed to traditional objected oriented programming.

The basic idea is that by leveraging asynchronous events, it is easier to model the fundamentally asynchronous nature of client-side web applications. Also, by writing components of your application to simply emit and handle events, it becomes a lot easier to extend or modify behavior down the road.

To illustrate this point, I’m going to show how you can build a tab widget using evented techniques, and show how it can be extended in several useful ways.

First, a few general principles

We’re going to use basic jQuery plugins to write the widget, but instead of putting all of the startup code directly inside the plugin constructor, we’re going to pass in an Object containing functions to be executed. In order to simplify matters, we’ll create a default set of functions to be executed containing the default behavior, but it will be possible to clone, extend, delete, or otherwise modify that set of functions and pass in an alternative.

The core of the tabs plugin will not handle any DOM manipulation. It will simply generate events that can be bound to the main tab node that will perform the manipulation. This increases the flexibility of the widget.

We’re also going to treat the main tabs node as a stateful container, holding information about the widget that we can retrieve later. We will achieve this using $().data(), an API added to jQuery in jQuery 1.2 and enhanced several times since then.

Finally, we’re going to use the executing anonymous closure trick ((function() { ... })()) to create a scratch-pad for helper functions that are used in various parts of the codebase. I use this trick frequently to create simple namespaces for internal methods.

To start (a quick detour)

Let’s start with a quick bit of sugar that I added to simplify setting and getting state out of a node. As I said above, jQuery already supplies the facilities for this using it’s internal data cache.

  var $$ = function(param) {
    var node = $(param)[0];
    var id = $.data(node);
    $.cache[id] = $.cache[id] || {};
    $.cache[id].node = node;
    return $.cache[id];
  };

Now, instead of doing $("#foo").data("foo") and ("#foo").data("foo", "bar"), you can do $$("#foo").foo and $$("#foo").foo = bar. The main motivation for this is that you can also do $$("#foo").bar = $$("#foo").bar + 1.

Step 1

The first step is to determine which events we’re going to need. After brainstorming a bit, we can come to:

  • An event each time a tab is activated. Parameter: the tab that was clicked
  • An event at startup time for each panel that is bound to a tab. Parameter: the panel
  • An event once startup is complete. No parameters.

The initial code looks like:

  $.fn.tabs = function(options) {
    options = options || {};
 
    // Initialize
    this.each(function() {
      var tabList = $(this);
      $$(tabList).panels = $();
 
      $("li a", tabList)
        .click(function() {
          tabList.trigger("activated", this);
          return false;
        }).each(function() {
          var panel = $($(this).attr("href"));
          $$(tabList).panels = $$(tabList).panels.add(panel);
          tabList.trigger("setupPanel", [panel]);
        });
 
      tabList.trigger("initialize");
    });
 
    return this;
  };

First, we get a reference to the tab list, since we will be creating callbacks later that will need access to it. Next, store a list of panels in the tab list using $$ (<code>$$(tabList).panels = $();</code>). We start by storing an empty jQuery object as “panels” in the tab widget.

Next, we bind a click handler to each <code>”li a”</code> inside the tab list ul. When it’s clicked, we simply trigger the activate event. We’ll implement the default behavior in a bit.

Finally, for each tab, we collect its associated panel and trigger the setupPanel event. The default behavior for setupPanel will simply be to hide it. We’ll implement that behavior in a bit as well.

Once we’re done, we trigger the panel’s initialize event.

Step 2

The next step is to declare the default functionality. Before we do that, let’s create a small jQuery helper that applies a set of functions to an object. You’ll see how it’s used in a moment.

jQuery.fn.setupPlugin = function(setup, options) {
  for(extra in setup) {
    var self = this;
    if(setup[extra] instanceof Array) {
      for(var i=0; i<setup[extra].length; i++) 
        setup[extra][i].call(self, options);
    } else {
      setup[extra].call(self, options);
    }
  }
};

This method is called on a jQuery object. It takes an object containing a set of setup methods, and the options that were passed into the plugin. For each key in the setup object, this method takes each function attached to it (the value for the key can either be a function or an Array of functions), and calls it with the jQuery object as “this” and the options as the first parameter.

What we’re going to do is call that method with a default set of methods (which we’ll call $.fn.tabs.base

this.setupPlugin(options.setup || $.fn.tabs.base, options);

Next, under the implemention (or in a separate file), define the defaults.

  var getPanel = function(selected) {
    return $($(selected).attr("href"));
  };
 
  $.fn.tabs.base = {
    setupPanel: [function(options) {
      this.bind("setupPanel", function(e, selector) {
        $(selector).hide();
      });
    }],
 
    initialize: [function(options) {
      this.bind("initialize", function() {
        var firstTab = $(this).find("li a:first")[0];
        $(this).trigger("activated", firstTab);
      });
    }],
 
    activate: [function(options) {
      this.bind("activated", function(e, selected) {
        var panel = getPanel(selected);
        $$(this).panels.hide();
        $(panel).show();
        $(this).find("li a").removeClass("active");
        $(selected).addClass("active").blur();
      });
    }]
  };

First, we define a small helper method that gets the associated panel for a tab. We could also have used the new $$ technique above to add it directly to the node.

Next, we define a few categories of methods to be run. Each of this methods will be triggered when we call setupPlugin, and takes the options hash as a parameter (optional). We will make each of the categories an Array, so they can be extended easily by prepending a setup method.

The setupPanel method binds the setupPanel event. If you recall, setupPanel takes the panel as its second parameter. As with all events, the first parameter is the event object, which we will not use now. When setupPanel is triggered, we will simply hide the panel. Note that we have decoupled the display details of the tab list from the events.

The next thing we do is handle initialization. In the simplest case, we’ll just always activate the first tab. To activate the tab, we’ll just trigger another event: “activated”.

Finally, we’ll define the default behavior for activation. When a tab is activated, we will:

  • Get the associated panel
  • Get all of the panels in the tab widget and hide them
  • Show the associated panel
  • Remove the “active” class from all tabs
  • Add the active class to this tab, and blur it

If you run this code, (available at <a href=”http://github.com/wycats/js_tabs_example/tree/master”>my github</a>”), you will have a basic tab widget set up and working!

Step 3: Add support for Ajax

Now that we have the basic functionality completed, let’s add support for Ajax. The API we’ll use it to support an option passed into the main widget like {xhr: {"#nameOfTab": "url_to_load"}}.

The first thing that we’ll do is make a clone of the default setup object so we can modify it.

var wycats = $.extend({}, $.fn.tabs.base);

Next, we’ll want to add a new function to run immediately before the default activated function. We still want to run the default activated function, which handles all the logic for displaying the tab, but we want to do some stuff first. Here’s the code:

  wycats.activate.unshift(function(options) {
    var xhr = options.xhr;
    this.bind("activated", function(e, selected) {
      var url = xhr && xhr[$(selected).attr("href")];
      if(url) {
        var panel = getPanel(selected);
        panel.html("<img src='throbber.gif'/>").load(url);
      }
    });
  });

Now you can see why we pass the options into the setup methods. First, we store off the xhr options into a local variable, which will be available to callbacks. Next, we bind the activated event. Note that since this is event, all bound events (including the default activated event from above) will get triggered. We’re using unshift here to bind this activated event first, which will cause it to get triggered before the default behavior later.

When the tab is activated, we check to see whether the tab’s href property is listed inside the xhr option. If it is, we replace the HTML of the panel with a throbber, and load the URL into it.

Step 4: Adding support for history

Finally, let’s add support for modifying the hash tag as we click on tabs, and loading the right tab on startup.

  delete wycats.initialize;
  wycats.hash = [function(options) {
    var tabs = this;
 
    this.bind("initialize", function() {
      var tab = $(this).find("li a");
      if(window.location.hash) tab = tab.filter("a[href='" + window.location.hash + "']");
      $(this).trigger("activated", tab[0]);
    });
 
    this.bind("activated", function(e, selected) {
      window.location.hash = $(selected).attr("href");
    });
  }];

We add a new category of functionality called “hash”. First, we delete the default initialize function, because it forces the first tab to be activated no matter what. We want to activate the tab that is present in the hash value (window.location.hash).

On initialization, we first get all of the tabs. Next, we check to see whether window.location.hash is populated. If it is, we filter the list of tabs to include only the one that matches the hash. Next, we activate the first remaining tab.

When a tab is activated, we update the hash to match the tab’s href.

Finally, because there is no generic “hashchange” event in the browser, we need to emulate one (in case the user presses the back button after clicking a tab):

    var lastHash = window.location.hash;
    setInterval(function() {
      if(lastHash != window.location.hash) {
        var tab = $(tabs).find("li a[href='" + window.location.hash + "']");
        if(!tab.is(".active")) $(tabs).trigger("activated", tab[0]);
        lastHash = window.location.hash;
      }
    }, 500);

Every 1/2 second, we check to see the if the hash has changed. If it did, and the tab is not yet active (which would happen if the user explicitly triggered the event), we trigger the activated event.

Wrapup

The basic idea here is that we have created an extensible tab system. The core of the tab system is just the necessary events, and we added a set of default event handlers for the events. Adding functionality, for the most part, simply required binding additional functionality to those events, and occasionally deleting a handler.

There are a few unconventional techniques here ($$, setupOptions), but they make it easier to implement the basic idea espoused in this article. It’s only one possible implementation, and I’d love to hear about tweaks to this approach or entirely different event-driven designs.

Better Module Organization

As I said in my last post, Carl and I have been working on a more modular version of ActionController. As we fleshed out the feature set, we had a few needs that aren’t directly addressed by Ruby’s built-in feature-set.

  • Modules occasionally depended on other modules (there’s no point in having Layouts without Renderer), but including Renderer into Layout meant that we couldn’t have setup on Renderer that got applied to the controller class itself. In this case, Renderer adds a “_view_paths” inheritable accessor to new Controller classes that is used to store a list of paths containing templates. If we included Renderer into Layouts, and then Layouts into ActionController::Base, that setup would happen on Layouts, which is wrong.
  • We used the def self.included(klass) klass.class_eval { ... } end idiom a whole lot. In fact, that’s the only thing we used the included hook for, except…
  • Extending ClassMethods onto the class.

When I was at Locos X Rails, I spent some time with Evan, and he argued that using the included hook should be done only after trying other abstractions. After speaking for a few minutes, Evan suggested abstracting away the above ideas in a higher-level abstraction that wrapped include. We could then more directly control the inclusion process, and even add our own hooks where needed.

I ended up with:

module AbstractController
  module Callbacks
    setup do
      include ActiveSupport::NewCallbacks
      define_callbacks :process_action
    end
    ...
  end
end

replacing:

module AbstractController
  module Callbacks
    def self.included(klass)
      klass.class_eval do
        include ActiveSupport::NewCallbacks
        define_callbacks :process_action
        extend ClassMethods
      end
    end
    ...
  end
end

For dependencies, I replaced:

module AbstractController
  module Helpers
 
    def self.included(klass)
      klass.class_eval do
        extend ClassMethods
        unless self < ::AbstractController::Renderer
          raise "You need to include AbstractController::Renderer before including " \
                "AbstractController::Helpers"
        end
        extlib_inheritable_accessor :master_helper_module
        self.master_helper_module = Module.new
      end
    end
    ...
  end
end

with

module AbstractController
  module Helpers
    depends_on Renderer
 
    setup do
      extlib_inheritable_accessor :master_helper_module
      self.master_helper_module = Module.new
    end
    ...
  end
end

And finally, the Base controller itself could now be replaced with:

module ActionController
  class Base2 < AbstractBase
    use AbstractController::Callbacks
    use AbstractController::Helpers
    use AbstractController::Logger
 
    use ActionController::HideActions
    use ActionController::UrlFor
    use ActionController::Renderer # just for clarity -- not required
    use ActionController::Layouts
  end
end

from:

module ActionController
  class Base2 < AbstractBase
    include AbstractController::Callbacks
    include AbstractController::Renderer
    include AbstractController::Helpers
    include AbstractController::Layouts
    include AbstractController::Logger
 
    include ActionController::HideActions
    include ActionController::UrlFor
    include ActionController::Layouts
    include ActionController::Renderer
  end
end

It’s not a tremendous change, but it definitely reduces the likelihood of accidental mistakes, and makes the actual usage a lot clearer. Of course, we will need to document this new mechanism, but it has already simplified the necessary mental model of the setup.

As always, thanks for reading!