This post is part of a series on event-driven programming. The complete series is:
- Events: they’re not just for the DOM, you know
- Observable objects
- Deferrable values
- Asynchronous methods
- First-leg round-up and final remarks
- Object lifecycle
- Asynchronous pipelines
- Testing event-driven apps
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 tochange
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:
- When the user clicks a ‘Connect with Facebook’ link, they should login with Facebook and be redirected to the homepage.
- When they complete a Facebook login or cancel the process, we should log these events using KISSmetrics.
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:
- It leaves you free to vary the underlying HTML used to model the widget. For many complex UIs, there are several totally reasonable ways of representing them in HTML. Your markup will be subject to pressures from many directions: CSS, SEO concerns, accessibility, cross-browser requirements, new HTML features and so on. Treating widgets as native HTML objects necessarily exposes their implementation and makes them much harder to change.
- Giving a widget a pure-JavaScript API makes it much easier to provide
alternative UIs for it, since all you need is some tiny DOM event handlers
that call the API. A great example of this is the
Paginator
class in Ojay. It exposes enough of its markup to let you style it, but its behaviour is hidden behind an API and its default control UI is totally decoupled from the widget’s core implementation. We could easily write new UIs for it and repurpose the default UI code to control other widgets with similar APIs. This is duck-typing at its best. - Modelling the events in JavaScript without using the DOM means you don’t
need to worry about cancellation or bubbling, or which DOM node is
responsible for firing the event. Users of the class just want to know that
their
Paginator
object fired ascroll
event; how the scroll is implemented in the DOM is of little concern. - Finally, the combination of a sensible scripting API and a good set of
events makes it easier to design components that can be trivially plugged
together without their code being tightly coupled. This is a cornerstone of
Ojay’s design, and is especially apparent in how easily its widgets
integrate with the
History
module. This composability does not just apply to user interface elements, but to domain models and data services components like Ajax.
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.