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.

@yukihiro_matz http://t.co/A5dXWqiX "You don't have to do everything that you can think of" I think of this when I hear of private_const

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.

23 Responses to “Evented Programming With jQuery”

Excellent post. I’ve been using this approach for a while, and I’ve started tinkering with a framework that tries to deal with it at a higher level of abstraction. It’s still in the early stages, but you can have a look here: http://github.com/nakajima/jquery-sod

Really great writeup.

What do you think about name-spacing the events like the way jQuery UI does? Is it enough to use the element that you’re binding to as a scope of an event? ‘initialize’ and ‘activated’ seem like vague event names if their bound to the global scope.

Also: I’m totally stealing $$ for a bunch of projects.

Absolutely. I used un-namespaced names for simplicity in the demo. In real life you would use namespaces :)

So, as a ruby/rails developer I ask “where are the tests?” :)

Do you use any testing environment when you do your javascript programming or is there nothing out there that’s very good for that?

Of course there are testing frameworks for JavaScript :) quite a few actually, have a look at JSpec http://visionmedia.github.com/jspec/

I also really like $$.

Might not have been the intention, but I read this post as a slight against the bloat of jQueryUI. I’ve found much provided by that endeavour overweight and not nearly as flexible as rolling my own components. which, as evidenced by this post can still be quite concise.

Nice :)

It would be great to read about adding tests/specs to this evented approach. Maybe second part of this will introduce screw unit to this tab plugin? :)

Is the full implementation (hopefully with a demo) available for download somewhere?

Great read. This is a very different approach from any of the plugins I’ve written, so it’s given me a lot to ponder. I’m digging the event-driven approach and how extensible it makes the plugin.

The piece that I had to read many times to understand how and why it is being used is the setupPlugin method. It still feels a bit overly complex for what it accomplishes, but I don’t have a better solution that allows prepending to the event chain.

One other thing I would do slightly differently is trigger a separate event when the hash changes so that anything could hook into it. That could be (and probably already is) a separate plugin.

Not trying to be critical though — this post gives me a lot of inspiration and new ideas. Thanks!

I absolutely agree that setupPlugin is overkill for this individual case. I use it for a number of plugins and treat it like a black box for this pattern.

This is the first article/tutorial on jQuery I’ve seen in months that was actually new, creative, and informative.

Thank you for expanding my depth of knowledge on jQuery. :-)

Thank you, realy great post! Good idea is to separate DOM and event driving on different layers of abstraction. This is new pattern for plugin writing!

Hi Yehuda,

I don’t quite get the getPanel function;
You pass a DOM link into it, you retrieve the href attribute but then you also convert that back to a jQuery object? Why that?

And then you convert it another time to a jQuery object at line 105 (line 6) while it is allready a jQuery object.. Or am i missing something?

Thanx
Mike

@mike the link looks like this .

Doing $(“#first”) gets a DOM element with ID “first”, so this is how we connect the panels to the links.

Great tip. The $$ helper is pretty rad. I’d recommend using a different name though to avoid conflicts with other libraries. I used $o (for object).

@Mike I was also a bit confused by that part. Would love to see it explained a bit more.

This is really great. I’ve been trying to figure out a good way to develop complex widgets in jQuery, and this pattern does the best job I’ve seen of allowing functionality to be overridden without emulating classical inheritance.

Also: this is obvious from looking at the source, but the examples on Github require jQuery 1.3.3pre. I was getting some errors with 1.3.2 before I noticed this. So make sure you have the correct version.

@Jim Benton,

Use “$$(tabList).panels = $([]);” to run this on JQuery 1.3.2

Excellent post. Thanks Yehuda.

@mike, @Tom
$(this).attr(“href”) would return -> “#first”

you then pass that as a selector to jQuery -> $(“#first”)

I often use $(this.hash) cause I feel it’s more explicit and faster than wrapping _this_ with jQuery.

I hope that’s clearer.

Leave a Reply