Saturday, December 22, 2007

Javascript DOM Event Handlers - The Right Way

What?
In his YUI theatre presentation An Inconvenient API, Douglas Crockford gives a great rundown of browser history, inner workings, and shortcomings. Among other things he gave a simple, cross-compatible way of doing javascript event handling in the browser.

Why?
Event handling is an excellent pattern, attested by its standardization and widespread use. However, due to browser incompatibilities it is not always obvious how to do it in javascript.

How?
First off, there are three ways of assigning event listeners to nodes: the old way, the IE way, and the W3C way. The type variable should always be an event name, e.g. 'click', 'mouseover', etc. See PPK's thorough listing of event names and compatibility notes at Quirksmode.


// IE way - non-standard
function ieEvent(node, type, handler) {
node.attachEvent('on'+type, handler);
}

// W3C way - long name and with a third, useless parameter that *must* be specified (undefined is not good enough)
function w3cEvent(node, type, handler) {
node.addEventListener(type, handler, false);
}

// Classic way - works in all A-grade browsers
function classicEvent(node, type, handler) {
node['on'+type] = handler;
}


Since the classicEvent way works in all Yahoo classified A-grade browsers Douglas recommends to simply use that way. How we process an event is still different across browsers however - in W3C our event handler gets passed an event object, while in IE we have to get the global event object. We deal with these incompatibilities as follows:


// A Cross browser event handler
function normalizedEventHandler(fn) {
// Get the event object - passed as argument in W3C, global object in IE.
e = e || event;
// Figure out what element the event happened to - again, disparate naming.
var target = e.target || e.srcElement;
// Now do your thing:
// ...
}


We now put it all together in a fail-safe, hopefully forwards-compatible event handling function:


/**
* Cross browser event handling function.
* @param {Element} node The dom node to attach the event handler to
* @param {String} type The type of event, e.g. 'mousedown', 'mousemove', 'resize'
* @param {Function} handler The function to be called when the event happens. It gets passed an event object e, as well as a target node.
*/
function addEvent(node, type, handler) {
// Create our normalized event handler
var normalizedHandler = function(e) {
// Get the event object - passed as argument in W3C, global object in IE.
e = e || event;
// Get the element the event happened to
var target = e.target || e.srcElement;
// Now call the handler with the normalized event object
handler(e, target);
}
// Assign the event handler in a way our browser understands
if (node.addEventListener) {
node.addEventListener(type, normalizedHandler, false);
} else if (node.attachEvent) {
node.attachEvent('on'+type, normalizedHandler);
} else if (node['on'+type]) {
node['on'+type] = normalizedHandler;
}
}
Finally, as a side note: wtf is the third parameter in W3C's addEventListener method? It has with event capturing and bubbling to do. An event gets called on a node, then its parent, then its parent, and so on until the event is canceled or the root node is reached. This is called bubbling.

Netscape implemented the reverse, called trickling down, which visits the parent first, and then goes down until the target node. This turns out to be simply wrong. What did W3C decide to do? Both, of course. This is it: the third parameter says whether you should do capturing the right way (i.e. bubbling, by setting it to false) or the wrong way (i.e. trickling, by setting it to true).

Bottom line is, if you don't know what capturing and event bubbling is, you will want it to be false. Always. To read more on event capture and bubbling, see this article.

Quickly, to cancel a bubbling cross-browser:

// Don't bubble up the event, i.e. "The event has been handled" or "Don't tell my parents"
function cancelBubble(e) {
// IE way
e.cancelBubble = true;
// Everyone else's way
if (e.stopPropagation) {
e.stopPropagation();
}
}
Many thanks, as always, to Douglas Crockford for the excellent information.

No comments: