The If Works This dirt was a building before

Events: they’re not just for the DOM, you know

This post is part of a series on event-driven programming. The complete series is:

Over recent months we’ve seen the major JavaScript libraries talking up their event support. Back in October Luke Smith spoke about the YUI3 event system, and more recently Yehuda Katz gave a screencast on evented programming with jQuery. Dan Webb took him to task for over-zealous use of data-* attributes, arguing that the DOM is not the place to model your domain. I have a similar but more broad problem with the screencast, which is that I think the DOM is not the place for your application’s event model.

Yehuda’s talk runs through an example of creating a tabs widget using jQuery. The assertion throughout is: wouldn’t it be nice if you could treat the events in the UI widget – events like changing the visible pane, clicking a tab etc. – just like you treat DOM events. This is true up to a point, in that having a consistent event API is nice, but it fails by trying to overload the DOM with too much responsibility and creates a leaky abstraction in the process.

The initial proposition is: imagine if HTML natively supported tabs. What would that look like? One suggestion is:

<tabs>
  <tab pane="first">First</tab>
  <tab pane="second" selected="selected">
    Second
  </tab>
  <tab pane="third">Third</tab>
</tabs>

<pane name="first">Some content</pane>
<pane name="second">Some content</pane>
<pane name="third">Some content</pane>

<script type="text/javascript">
  $("tabs").bind("change", function() {
    $(this).attr("panes").hide();
    $(this).attr("selected").show();
  });
</script>

Now, the above elements don’t exist in HTML so instead it’s proposed that we get as close as we can in real HTML like so:

<ul class="tabs">
  <li data-pane="first">First</li>
  <li data-pane="second" class="selected">
    Second
  </li>
  <li data-pane="third">Third</li>
</ul>

<div class="pane" id="first">Some content</div>
<div class="pane" id="second">Some content</div>
<div class="pane" id="third">Some content</div>

<script type="text/javascript">
  // When tab selection changes, show all
  // the panes then hide the selected one
  $("ul.tabs").bind("change", function() {
    $(this).attr("panes").hide();
    $(this).attr("selected").show();
  });
</script>

This won’t work out of the box so some plumbing is needed to make ul elements support change events and tab-related attributes:

// Trigger 'change' when 'click' fires
$("ul.tabs").click(function() {
  $(this).trigger("change");
});

// On change, set the selected pane and
// toggle some class names
$("ul.tabs").bind("change", function(e) {
  $(this).attr("selected", e.target);

  $(e.target)
    .addClass("selected")
    .siblings()
    .removeClass("selected");
});

$(document).ready(function() {
  // Set the 'panes' attribute by getting the divs
  // referenced by the li 'data-pane' attributes
  $("ul.tabs").attr("panes", function() {
    return $("li", this).map(function() {
      return $("#" + $(this).attr("data-pane"));
    });
  });

  $("ul.tabs").trigger("change");
});

Don’t try this code out; even after correcting some of the more obvious typos from the screencast I still can’t get it to work. There are missing attributes (e.g. the selected attribute is never set), and in any case the DOM stringifies anything you set as an attribute so the expression $(this).attr("panes") just returns "[object Object]" rather than a list of DOM nodes.

But the thing I really want to focus on here is evented programming, and how in JavaScript there’s this unwritten assumption that if you need events, you need the DOM. In fact Yehuda nails it with this quote:

The nice thing about the browser code that we’re implementing is that it’s very familiar to you. You know how to do $("ul.tabs").click(), you know how to bind to change and add classes and get siblings. This is code that you’re already familiar with.

This reminds me of the classic quote, “When your hammer is C++, everything begins to look like a thumb.” Just because you know how one thing works, that doesn’t make it a good model for every programming task. Think about what an event is for a second. An event is just a point in time at which some change takes place. Yes, the DOM fires events all the time as changes are made to it by scripts and user interaction; these events are what power GUIs on the web. But not every event in your application needs to live in the DOM.

Let’s take a concrete example. Suppose part of your client-side app is a module that integrates your application with Facebook Connect. Your requirements list includes such items as:

The spirit of Yehuda’s talk is right in that we should translate this into real code that’s close to how we wrote the requirements (by the way, TDD is a great way to get into this habbit):

$("a.facebook-connect").click(function() {
  Facebook.loginAndRedirect("/");
  return false;
});

Facebook.on("login", function() {
  KM.record("Login with Facebook");
});

Facebook.on("cancel", function() {
  KM.record("Cancel Facebook login");
});

When we get around to implementing our Facebook module, how should we support these events? The first response of most JavaScripters seems to be, “Use the DOM.” But what if your events have nothing to do with the DOM? This module is a wrapper around what is essentially a web service that provides authentication and data APIs. If you had to pick a DOM node to fire these events, which would you pick? Would the fact that DOM events bubble and can be cancelled cause you problems? Do these concepts even make sense for the task at hand? My experience is that for most application-level events, they do not.

The way I like to think of the DOM is as an output device, and nothing more. I model my UI widgets and their events in pure JavaScript, providing API methods for everything the widget can do and all the events it supports. Internally, the widget takes care of hacking the DOM in whatever ways are needed to get the right look and feel. This has several benefits:

Part of Yehuda’s argument is that an object-oriented approach to GUIs doesn’t work, that writing code like

Tabs = new Class({
  initialize: function(node) {
    this.node = node;
  },
  select: function(tab) {
    // perform selection
    // behavior
  }
});

is bad because method calls assume synchronous rather than event-driven behaviour. This does not have to be the case, and as I outlined above objects are a great way to package up components and give them scripting APIs for easy integration. With that in mind, I’m going to spend this week on a series of articles on event-driven programming and common patterns for combining it with objects. The first will be on implementing events without any help from the DOM – see you tomorrow.


4 Comments

Great article.

I use Prototype a fair bit, which suffers from the same limitations – you can subscribe to, and fire “custom” events, but only on DOM elements.

Can’t wait to read the rest in your series!

Posted by Mike Rumble on 21 February 2010 @ 11pm

Great article, James.

Mike, for the record, any true event handling system written in JavaScript will need to rely on host objects for error handling (you don’t want an error thrown by an event handler to stop the event being dispatched to the other event handlers). Dean Edwards wrote a great article on the subject a while back: http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/

Prototype’s event model uses the DOM, other systems will rely on `setTimeout` to call handlers. Either way, you’re bound to the browser environment.

That said, if you consider the `document` object as as the broker in a publisher/subscriber pattern, you can very well use Prototype’s custom event system outside of the DOM.

Posted by Tobie Langel on 22 February 2010 @ 11am

Thanks for the reminder about Dean’s article, it’s definitely worth a read. I may amend the first article I’ve drafted to cover robustness, or leave concerns like that for a final round-up piece on the all the patterns I’ll cover. I want to keep the initial implementation examples really simple to communicate the basic ideas in a fairly language-neutral way.

Posted by James Coglan on 22 February 2010 @ 11am

@Tobie, thanks for the info – wasn’t so much referring to the implementation details, but more to the fact that the API exposed by Prototype, by default only allows custom events on DOM elements.

The approach of using the ‘document’ is a neat one, and I’ve seen a mixin you created a while back (http://gist.github.com/204012) that does just that. Would love to have this kind of functionality rolled in to Prototype’s core.

Posted by Mike Rumble on 22 February 2010 @ 7pm

Leave a Comment