Archive for February, 2008

Modularity in jQuery: Part II

Last time, we talked about using event bindings to provide a publish/subscribe mechanism for your plugins, and introduce modularity via the event system.

This time, we’ll talk about two new features added since jQuery 1.2: Setup and teardown for events and the element data store. First up, the element data store:

Prior to jQuery 1.2, data was added to elements via expandos. If, for instance, d was an element, and we wanted to add some data named “foo” with the value “bar”, we would do d.foo = "bar". Simple enough. But there are two major issues:

  • Other libraries, plugins, or widgets might try and use the foo name. We could try and obfuscate the name (jQuery 1.0 used $events as the expando name for the events array on an element), but there was still a chance that another library could accidentally use the same name. In fact, that’s exactly what happened. MooTools inadvertently used $events, and people who tried to use both libraries got hosed.
  • IE’s garbage collection gets all flummoxed under certain circumstances when an element is removed but its expandos still have references to live objects.

The solution, which jQuery adopted for 1.2, is to create a new data store that’s independent of the element and in the jQuery namespace, and which jQuery garbage collects itself when you remove the associated elements. This provides a mechanism for tagging elements with information that can then be used by different modules in your program. While jQuery uses it to track which events belong to which elements, it can also be used for other purposes.

For instance, the jQuery metadata plugin allows you to specify data to be stored in the data store in your markup. Here’s an example:

The markup

<div data="{foo: 'bar'}">Some div element is here</div>

The code

$.metadata.setType("attr", "data");
$("div").metadata().foo == "bar"

The metadata plugin produces a thin sheen over the data-store, allowing you to pass information into the store via markup. That might be quite useful for plugins, to allow your users to specify options for your plugin without needing to write any special code. For instance, you could write this code in your plugin:

Markup

<div class="myPlugin" data="{myPlugin: {option1: 12, option2: "stringyString"}}">Some Text</div>

The Code

$.metadata.setType("attr", "data");
jQuery(function($) {
  $("div.myPlugin").each(function() {
    $(this).myPlugin($(this).metadata().myPlugin);
  });
});

This is assuming, of course, that there is some $.fn.myPlugin that takes option1 and option2 as options. This technique allows you to put the initialization code in your plugin itself and let the user interact with it in fairly high-level ways via markup. The TableSorter plugin makes use of this technique.

But you can use the data store for other purposes as well. In particular, you can interact directly with the data store by doing $.data(el, “key”, “value”) to set a key in the store, or $.data(el, “key”) to retrieve a key. jQuery 1.2.3 will also add a new shortcut (which we’ll discuss in more length in the next post in this series), which will allow $(”div”).data(”key”).

I use this technique in the autocomplete plugin to tag a keystroke as “final” (hitting escape, for instance, should not trigger a new dropdown if it’s used to cancel the autocomplete). Even though the autocompleter is in a separate module from the event code that triggers the autocompleter, this information can be tagged onto the element so it’s accessible from any module.

Setup and Teardown

Setup and teardown lets you further customize event binding. In effect, setup and teardown functions are called immediately before an event is bound (and immediately before an event is unbound), allowing you to preform processing. Returning true from the setup function causes it to get bound using the browser’s native handlers, instead of the jQuery event system (don’t worry about this; it’s extremely rare). Let’s take a hypothetical activate.myPlugin event.

The Code

$.event.special["activate.myPlugin] = function() {
  /* “this” is the element being bound */
  /* do some processing */;
}

Later, when you do $("div").bind("activate.myPlugin") the special method will get run. This allows your plugin’s users to add some processing to when your plugin gets bound to an element, without having to override the plugin binding method.

Next up: New features that will be introduced in 1.2.3, and bringing this all together :)

Modularity in jQuery

There have recently been several comments along the lines that jQuery is not very modular, once you get past modifying DOM elements. I get why people are saying that, but there’s been a lot of features added to jQuery over the past year or so that provide a compelling architecture for building modular apps.

It’s not exactly OO, which is what a lot of people are used to; it’s more like a publish/subscribe model, with points of modularity added via message passing. That’s because jQuery provides a pretty kick-ass system for custom events (events that are not built-into the browser), including a mechanism to subscribe to such events and a mechanism to trigger (i.e. publish) the events, with custom data.

It also provides the potential for setup and teardown for both browser and custom events, as well as a central data store for persistent data that would traditionally be attached directly to an element. This solves both the memory-leak issue (jQuery manages garbage collection of objects in the store that are no longer valid because the DOM element doesn’t exist anymore) as well as a crucial compatibility issue (we don’t store the data in the element itself, so we work fine with other libraries that do, or other libraries that have their own store in their own namespace).

Events

If you use jQuery, you already know about browser events:

$("div").click(function() { /* code here */ });

That’s a shortcut for:

$("div").bind("click", function() { /* code here */ });

What you probably don’t know is that you can also bind your own custom events:

$("div").bind("clickeroo", function() { /* code here */ });

That probably doesn’t seem all that useful until you realize you can also trigger these custom events:

$("div").trigger("clickeroo")

or even:

$("div").bind("clickeroo", function(e, data1, data2) {
  // some stuff that uses data
});
$("div").trigger("clickeroo", [dataObject1, dataObject2]);

And finally, you can bind custom events with namespaces:

$("div").bind("activate.dialog", function(event, options) {
  // some stuff that uses options
});

Triggering activate or activate.dialog will trigger the event, and because all JS arguments are optional, the options argument will work whether or not the core event was triggered with the additional data.

Where does this bring us?

So I claimed earlier that this provides us with a useful tool for modularity. If you’ve been paying attention, and like me, have run into some modularity ceilings, you should be realizing some of the possibilities here. Effectively, what you have is a publish/subscribe model for events. If I create a dialog plugin, I don’t have to have a massive options hash to provide a callback for the dialog opening, the dialog closing, the dialog resizing, etc. All I need to do is blindly trigger open.dialog, close.dialog, resize.dialog, etc. and allow the user to subscribe to those events if necessary. This even allows for meta-plugins that simply subscribe to events published by the main plugin and extend them. This is the approach I take in my autocomplete plugin, which publishes “autocomplete.activate”, “autocomplete.cancel”, and “autocomplete.selected” events.

My plugin also subscribes to the “updateList.autocomplete” event, which allows outside code to provide a list for the autocompleter at any time, without having to know how the internal autocomplete code works. In fact, the Ajax extension to the autocomplete plugin simply registers a callback for the call to getList that the plugin calls, which publishes an updateList.autocomplete event with the appropriate data when done.

In the end, this allows you to create self-contained modules in your application that communication with each other by providing public publish and subscribe points for interaction. In the case of the autocomplete plugin, all the events are published onto the input element, and external interfaces subscribe to events published to that element. This allows multiple autocomplete plugins on a single page that don’t need to use a single message bus. However, for more global events, you can publish/subscribe to/from the window or document object, which can provide a more global event bus.

Next time, I’ll talk about the setup and teardown methods for custom events, as well as using the element data store to store data about an element that needs to be shared between modules.