Events are at the core of your JavaScript application, powering everything and providing the first point of contact when a user interacts with your application. However, this is where JavaScript’s unstandardized birth rears its ugly head. At the height of the browser wars, Netscape and Microsoft purposely chose different, incompatible event models. Although they were later standardized by the W3C, Internet Explorer kept its different implementation until its latest release, IE9.
Luckily, we have great libraries like jQuery and Prototype that smooth over the mess, giving you one API that will work with all the event implementations. Still, it’s worth understanding what’s happening behind the scenes, so I’m going to cover the W3C model here before showing examples for various popular libraries.
Events revolve around a function called addEventListener(), which takes three arguments: type (e.g., click),
listener (i.e., callback), and useCapture (we’ll cover useCapture later). Using the first two
arguments, we can attach a function to a DOM element, which is invoked
when that particular event, such as click, is
triggered on the element:
var button = document.getElementById("createButton");
button.addEventListener("click", function(){ /* ... */ }, false);We can remove the listener using removeEventListener(), passing the same arguments we gave addEventListener(). If the listener function is
anonymous and there’s no reference to it, it can’t be removed without
destroying the element:
var div = document.getElementById("div");
var listener = function(event) { /* ... */ };
div.addEventListener("click", listener, false);
div.removeEventListener("click", listener, false);As its first argument, the listener function is passed an event object, which you can use to get
information about the event, such as timestamp, coordinates, and target.
It also contains various functions to stop the event propagation and
prevent the default action.
As for event types, the supported ones vary from browser to browser, but all modern browsers have the following:
click
dblclick
mousemove
mouseover
mouseout
focus
blur
change (for form inputs)
submit (for forms)
Check out Quirksmode, which has a full event compatibility table.
Before we go any further, it’s important to discuss event ordering. If an element and one of its ancestors have an event handler for the same event type, which one should fire first when the event is triggered? Well, you won’t be surprised to hear that Netscape and Microsoft had different ideas.
Netscape 4 supported event capturing, which triggers event listeners from the top-most ancestor to the element in question—i.e., from the outside in.
Microsoft endorsed event bubbling, which triggers event listeners from the element, propagating up through its ancestors—i.e., from the inside out.
Event bubbling makes more sense to me, and it is likely to be the model used in day-to-day development. The W3C compromised and stipulated support for both event models in their specification. Events conforming to the W3C model are first captured until they reach the target element; then, they bubble up again.
You can choose the type of event handler you want to register,
capturing or bubbling, which is where the useCapture argument to addEventListener() comes into the picture. If
the last argument to addEventListener()
is true, the event handler is set for
the capturing phase; if it is false,
the event handler is set for the bubbling phase:
// Use bubbling by passing false as the last argument
button.addEventListener("click", function(){ /* ... */ }, false);The vast majority of the time, you’ll probably be using event
bubbling. If in doubt, pass false as
the last argument to addEventListener().
When the event is bubbling up, you can stop its progress
with the stopPropagation()
function, located on the event
object. Any handlers on ancestor elements won’t be invoked:
button.addEventListener("click", function(e){
e.stopPropagation();
/* ... */
}, false);Additionally, some libraries like jQuery support a stopImmediatePropagation() function, preventing any further handlers from being called at
all—even if they’re on the same element.
Browsers also give default actions to events. For example, when you click on a link,
the browser’s default action is to load a new page, or when you click on a
checkbox, the browser checks it.
This default action happens after all the event propagation phases and can
be canceled during any one of those. You can prevent the default action
with the preventDefault() function
on the event object.
Alternatively, you can just return false from the handler:
form.addEventListener("submit", function(e){
/* ... */
return confirm("Are you super sure?");
}, false);If the call to confirm() returns
false—i.e., the user clicks cancel in the confirmation dialog—the event
callback function will return false, canceling the event and form
submission.
As well as the aforementioned functions—stopPropagation() and preventDefault()—the event object contains a lot of useful
properties. Most of the properties in the W3C specification are documented below; for more
information, see the full
specification.
Type of event:
Properties reflecting the environment when the event was executed:
buttonA value indicating which, if any, mouse button(s) was pressed
ctrlKeyA boolean indicating whether the Ctrl key was pressed
altKeyA boolean indicating whether the Alt key was pressed
shiftKeyA boolean indicating whether the Shift key was pressed
metaKeyA boolean indicating whether the Meta key was pressed
Properties specific to keyboard events:
isCharA boolean indicating whether the event has a key character
charCodeA unicode value of the pressed key (for keypress events only)
keyCodeA unicode value of a noncharacter key
whichA unicode value of the pressed key, regardless of whether it’s a character
Where the event happened:
The event coordinates relative to the page (i.e., viewport)
The event coordinates relative to the screen
Elements associated with the event:
currentTargetThe current DOM element within the event bubbling phase
target, originalTargetThe original DOM element
relatedTargetThe other DOM element involved in the event, if any
These properties vary in browsers, especially among those that are not W3C-compliant. Luckily, libraries like jQuery and Prototype will smooth out any differences.
In all likelihood you’ll end up using a JavaScript library for event management; otherwise, there are just too many browser inconsistencies. I’m going to show you how to use jQuery’s event management API, although there are many other good choices, such as Prototype, MooTools, and YUI. Refer to their respective APIs for more in-depth documentation.
jQuery’s API has a bind()
function for adding cross-browser event listeners. Call this function on jQuery instances, passing in an event
name and handler:
jQuery("#element").bind(eventName, handler);For example, you can register a click handler on an element like so:
jQuery("#element").bind("click", function(event) {
// ...
});jQuery has some shortcuts for event types like click, submit, and mouseover. It looks like this:
$("#myDiv").click(function(){
// ...
});It’s important to note that the element must exist before you start adding events to it—i.e., you should do so after the page has loaded. All you need to do is listen for the window’s load event, and then start adding listeners:
jQuery(window).bind("load", function() {
$("#signinForm").submit(checkForm);
});However, there’s a better event to listen for than the window’s load, and that’s DOMContentLoaded. It fires when the DOM is ready, but before the page’s images and stylesheets have downloaded. This means the event will always fire before users can interact with the page.
The DOMContentLoaded event isn’t supported in
every browser, so jQuery abstracts it with a ready() function that has cross-browser support:
jQuery(document).ready(function($){
$("#myForm").bind("submit", function(){ /* ... */ });
});In fact, you can skip the ready()
function and pass the handler straight to the jQuery object:
jQuery(function($){
// Called when the page is ready
});One thing that’s often confusing about events is how the
context changes when the handler is invoked. When using the browser’s
native addEventListener(), the context is changed from the local one to the targeted
HTML element:
new function(){
this.appName = "wem";
document.body.addEventListener("click", function(e){
// Context has changed, so appName will be undefined
alert(this.appName);
}, false);
};To preserve the original context, wrap the handler in an anonymous
function, keeping a reference to it. We covered this pattern in Chapter 1, where we used a proxy function to maintain the
current context. It’s such a common pattern that jQuery includes a
proxy() function—just pass in the function and context in which you want it
to be invoked:
$("signinForm").submit($.proxy(function(){ /* ... */ }, this));It may have occurred to you that since events bubble up, we could just add a listener on a parent element, checking for events on its children. This is exactly the technique that frameworks like SproutCore use to reduce the number of event listeners in the application:
// Delegating events on a ul list
list.addEventListener("click", function(e){
if( e.target.tagName == "li" )
{
return false;
}
});jQuery has a great way of doing this; simply pass the delegate() function a child selector, event type, and handler. The alternative
to this approach would be to add a click event to
every li element. However, by using
delegate(), you’re reducing the number
of event listeners, improving performance:
// Don't do this! It adds a listener to every 'li' element (expensive)
$("ul li").click(function(){ /* ... */ });
// This only adds one event listener
$("ul").delegate("li", "click", /* ... */);Another advantage to event delegation is that any children added
dynamically to the element would still have the event listener. So, in the
above example, any li elements added to
the list after the page loaded would still invoke the click
handler.
Beyond events that are native to the browser, you can trigger and bind them to your own custom events. Indeed, it’s a great way of architecting libraries—a pattern a lot of jQuery plug-ins use. The W3C spec for custom events has been largely ignored by the browser vendors; you’ll have to use libraries like jQuery or Prototype for this feature.
jQuery lets you fire custom events using the trigger() function. You can namespace event names, but namespaces are separated by full stops and reversed. For
example:
// Bind custom event
$(".class").bind("refresh.widget", function(){});
// Trigger custom event
$(".class").trigger("refresh.widget");And to pass data to the event handler, just pass it as an extra
parameter to trigger(). The data will
be sent to callbacks as extra arguments:
$(".class").bind("frob.widget", function(event, dataNumber){
console.log(dataNumber);
});
$(".class").trigger("frob.widget", 5);Like native events, custom events will propagate up the DOM tree.
Custom events, often used to great effect in jQuery plug-ins, are a great way to architect any piece of logic that interacts with the DOM. If you’re unfamiliar with jQuery plug-ins, skip ahead to Appendix B, which includes a jQuery primer.
If you’re adding a piece of functionality to your application, always consider whether it could be abstracted and split out in a plug-in. This will help with decoupling and could leave you with a reusable library.
For example, let’s look at a simple jQuery plug-in for tabs. We’re
going to have a ul list that will
respond to click events. When the user clicks on a list item, we’ll add an
active class to it and remove the
active class from the other list items:
<ul id="tabs"> <li data-tab="users">Users</li> <li data-tab="groups">Groups</li> </ul> <div id="tabsContent"> <div data-tab="users"> ... </div> <div data-tab="groups"> ... </div> </div>
In addition, we have a tabsContent div that contains the actual
contents of the tabs. We’ll also be adding and removing the
active class from the div’s children, depending on
which tab was clicked. The actual displaying and hiding of the tabs will
be done by CSS—our plug-in just toggles the active
class:
jQuery.fn.tabs = function(control){
var element = $(this);
control = $(control);
element.find("li").bind("click", function(){
// Add/remove active class from the list-item
element.find("li").removeClass("active");
$(this).addClass("active");
// Add/remove active class from tabContent
var tabName = $(this).attr("data-tab");
control.find(">[data-tab]").removeClass("active");
control.find(">[data-tab='" + tabName + "']").addClass("active");
});
// Activate first tab
element.find("li:first").addClass("active");
// Return 'this' to enable chaining
return this;
};The plug-in is on jQuery’s prototype, so it can be called on jQuery
instances:
$("ul#tabs").tabs("#tabsContent");What’s wrong with the plug-in so far? Well, we’re adding a
click event handler onto all the list items, which is
our first mistake. Instead, we should be using the delegate() function covered earlier in this
chapter. Also, that click handler is massive, so it’s difficult to see
what’s going on. Furthermore, if another developer wanted to extend our
plug-in, he’d probably have to rewrite it.
Let’s see how we can use custom events to clean up our code. We’ll fire a change.tabs event when a tab is clicked, and bind several handlers to change the active class as appropriate:
jQuery.fn.tabs = function(control){
var element = $(this)
control = $(control);
element.delegate("li", "click", function(){
// Retrieve tab name
var tabName = $(this).attr("data-tab");
// Fire custom event on tab click
element.trigger("change.tabs", tabName);
});
// Bind to custom event
element.bind("change.tabs", function(e, tabName){
element.find("li").removeClass("active");
element.find(">[data-tab='" + tabName + "']").addClass("active");
});
element.bind("change.tabs", function(e, tabName){
control.find(">[data-tab]").removeClass("active");
control.find(">[data-tab='" + tabName + "']").addClass("active");
});
// Activate first tab
var firstName = element.find("li:first").attr("data-tab");
element.trigger("change.tabs", firstName);
return this;
};See how much cleaner the code is with custom event handlers? It means we can split up the tab change handlers, and it has the added advantage of making the plug-in much easier to extend. For example, we can now programmatically change tabs by firing our change.tabs event on the observed list:
$("#tabs").trigger("change.tabs", "users");We could also tie up the tabs with the window’s hash, adding back button support:
$("#tabs").bind("change.tabs", function(e, tabName){
window.location.hash = tabName;
});
$(window).bind("hashchange", function(){
var tabName = window.location.hash.slice(1);
$("#tabs").trigger("change.tabs", tabName);
});The fact that we’re using custom events gives other developers a lot of scope when extending our work.
Event-based programming is very powerful because it decouples your application’s architecture, leading to better self-containment and maintainability. Events aren’t restricted to the DOM though, so you can easily write your own event handler library. The pattern is called Publish/Subscribe, and it’s a good one to be familiar with.
Publish/Subscribe, or Pub/Sub, is a messaging pattern with two actors, publishers, and subscribers. Publishers publish messages to a particular channel, and subscribers subscribe to channels, receiving notifications when new messages are published. The key here is that publishers and subscribers are completely decoupled—they have no idea of each other’s existence. The only thing the two share is the channel name.
The decoupling of publishers and subscribers allows your application to grow without introducing a lot of interdependency and coupling, improving the ease of maintenance, as well as adding extra features.
So, how do you actually go about using Pub/Sub in an application?
All you need to do is record handlers associated with an event name and
then have a way of invoking them. Here’s an example PubSub object, which we can use for adding and
triggering event listeners:
var PubSub = {
subscribe: function(ev, callback) {
// Create _callbacks object, unless it already exists
var calls = this._callbacks || (this._callbacks = {});
// Create an array for the given event key, unless it exists, then
// append the callback to the array
(this._callbacks[ev] || (this._callbacks[ev] = [])).push(callback);
return this;
},
publish: function() {
// Turn arguments object into a real array
var args = Array.prototype.slice.call(arguments, 0);
// Extract the first argument, the event name
var ev = args.shift();
// Return if there isn't a _callbacks object, or
// if it doesn't contain an array for the given event
var list, calls, i, l;
if (!(calls = this._callbacks)) return this;
if (!(list = this._callbacks[ev])) return this;
// Invoke the callbacks
for (i = 0, l = list.length; i < l; i++)
list[i].apply(this, args);
return this;
}
};
// Example usage
PubSub.subscribe("wem", function(){
alert("Wem!");
});
PubSub.publish("wem");You can namespace events by using a separator, such as a colon (:).
PubSub.subscribe("user:create", function(){ /* ... */ });If you’re using jQuery, there’s an even easier library by Ben Alman. It’s so short, in fact, that we can put it inline:
/*!
* jQuery Tiny Pub/Sub - v0.3 - 11/4/2010
* http://benalman.com/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
(function($){
var o = $({});
$.subscribe = function() {
o.bind.apply( o, arguments );
};
$.unsubscribe = function() {
o.unbind.apply( o, arguments );
};
$.publish = function() {
o.trigger.apply( o, arguments );
};
})(jQuery);The API takes the same arguments as jQuery’s bind() and trigger() functions. The only difference is that
the functions reside directly on the jQuery object, and they are called publish() and subscribe():
$.subscribe( "/some/topic", function( event, a, b, c ) {
console.log( event.type, a + b + c );
});
$.publish("/some/topic", ["a", "b", "c"]);We’ve been using Pub/Sub for global events, but it’s just as easy to
scope it. Let’s take the PubSub object
we created previously and scope it to an object:
var Asset = {};
// Add PubSub
jQuery.extend(Asset, PubSub);
// We now have publish/subscribe functions
Asset.subscribe("create", function(){
// ...
});We’re using jQuery’s extend() to
copy PubSub’s properties onto our
Asset object. Now, all calls to publish() and subscribe() are scoped by Asset. This is useful in lots of scenarios,
including events in an object-relational mapping (ORM), changes in a state
machine, or callbacks once an Ajax request has finished.