Attaching and Detaching Event Listeners
Attaching (and to a lesser extent detaching) event listeners is a
crucial part of most browser scripting applications. Do it wrong and
your application will likely be crippled and any end-user will tell
you a crippled application is worse than one that doesn't work at all.
The first question is what type of object(s) will need listeners
attached. Windows? Documents? Elements? All of these? This is not a
question to be taken lightly as attaching a listener to the wrong
object will often fail silently (or work in one browser but not the
next).
There isn't any practical way to cram all three of these scenarios
into one function. For one, there is no standard way to determine one
type of host object from another. That should shoot down the idea
right there. You will need at least one function per host object type.
Let's start with windows. The first thing you need to know is that
window objects were not part of the standard recommendations used by
the authors of many (if not most) browsers in use today. It doesn't
really matter if HTML5 "standardizes" the window object now as HTML5
exists only on paper. Perhaps in five years HTML5's recommendations
will be relevant to consider. For now, save for its history, the
window object is one big unknown.
// Degrades in IE 8-
// Also degrades in some older browsers that lack this method on
window objects
// No frames
// NOTE: Abbreviated for examples -- use isHostMethod to detect host
methods
if (window.addEventListener) {
var attachWindowListener = function(eventType, fn) {
window.addEventListener(eventType, fn, false);
};
}
Now the question is what sort of events would history and common sense
tell you belong to the window? These four come to mind:-
- load
- scroll
- resize
- orientationchange
You should never assume that other DOM events (e.g. click, mousedown)
will bubble up to the window object because there is no standard
recommendation that says they have to.
if (window.addEventListener) {
var attachWindowListener = function(eventType, fn) {
// Remove this line on deployment -- for debugging only
if (!(/^(load|scroll|resize|orientationchange)$/.test(eventType)))
{
throw new Error('Use attachListener with an element.');
}
window.addEventListener(eventType, fn, false);
};
}
Will all four of those work on the window object? Certainly can't say
for sure. ISTM that there has been at least one browser in the last
decade that required "scroll" listeners to be attached to the
document. So you would rarely use this directly, but through a wrapper
that attaches multiple listener types (e.g. attachReadyListener from
previous edition).
For another example, an attachOrientationListeners wrapper would
attach both "orientationchange" and "resize" listeners (and then deal
with only one type, determined the first time through). This is all
you can do as there is no reliable way to detect what sort of event
listeners will fire.
You can detect which DOM0 event handler attributes (e.g.
"ontouchstart") are supported (as seen in a dozens of variations of my
original test), but that is only for use with elements and doesn't
give you enough information to decide which types of listeners your
script should attach (unless it is using setAttribute to create event
handler attributes!)
As noted, the above renditions degrade in IE 8-. In some contexts,
that's perfect. In others, it's a show-stopper. You should be able to
create a forked rendition that uses attachEvent at this point. If not,
see my listener examples on JSPerf (link at end).
What about documents? The middle child is neglected; other than
DOMContentLoaded, there's typically nothing to do and therefore no
wrappers are needed.
That leaves elements:-
// Degrades in IE 8-
// Also degrades in some older browsers that lack this method on
window objects
// No frames
// NOTE: Abbreviated for examples -- use isHostMethod to detect host
methods
if (document.documentElement &&
document.documentElement.addEventListener) {
var attachListener = function(el, eventType, fn) {
el.addEventListener(eventType, fn, false);
};
var detachListener = function(el, eventType, fn) {
el.removeEventListener(eventType, fn, false);
};
}
How about "binding" the - this - object for the listener?
if (attachListener && Function.prototype.call) {
var bindListener = function(el, eventType, fn, thisObject) {
var listener = function(e) {
fn.call(thisObject || el, e);
};
attachListener(el, eventType, listener);
// NOTE: Returns bound listener function
return listener;
};
var unbindListener = function(el, eventType, boundListener) {
detachListener(el, eventType, boundListener);
};
}
Why include the seemingly redundant "unbindListener"? To reinforce
that it expects a previously bound listener, not the original
function.
That should close the book on "event registries". There's no point in
reproducing that functionality; just let the browser handle it. You
say you don't want to "keep track" of the returned function reference?
Just put it where you wherever you were going to track the original
function (you don't need it anymore).
What about delegation? The first thing you need is the event target:-
// Will not work in IE 8-, so use only with attachListener renditions
that degrade in IE 8-
function getEventTarget(e) {
var target = e.target;
// Check if not an element (e.g. a text node)
if (1 != target.nodeType) {
// Set reference to parent node (which must be an element)
target = target.parentNode;
}
return target;
}
...Or if you will be using a forked rendition to support IE 8-:-
function getEventTarget(e) {
var target = e.target;
if (target) {
// Check if not an element (e.g. a text node)
if (1 != target.nodeType) {
// Set reference to parent node (which must be an element)
target = target.parentNode;
}
} else {
target = e.srcElement;
}
return target;
}
You may also want the "related target" for certain pointer-related
events:-
function getRelatedEventTarget(e) {
var target = e.relatedTarget;
// Check if not an element (e.g. a text node)
if (1 != target.nodeType) {
// Set reference to parent node (which must be an element)
target = target.parentNode;
}
return target;
}
...but you will have to wait for the book (or dig through My Library)
for the IE rendition of that one.
Once you have the target, the standard procedure is to find an
ancestor matching a predetermined set of criteria (e.g. tag name,
class name, etc.) and use it for the - this - object when calling the
"listener" (really just an unattached callback function).
The signature looks like this:-
var delegateListener = function(el, eventType, fn, fnDelegate) {
// ...
}
Instead of setting of using a single reference for - this - for all
callbacks, a reference is supplied dynamically by the delegation
callback function. Will leave that as exercise. Note that it will have
to return a function, as in the "bindListener" function.
Also note that you could create a wrapper for this function called
"delegateListenerByQuery" and have the callback function run a query
to find the appropriate ancestor. Like most schemes involving queries,
this is not recommended at all past the mock-up stage. Event handling
needs to be done as simply and quickly as possible, which lets queries
out. Besides, there are no ancestor-based queries (except in a newer
version of MooTools I think). :) I think it's safe to say that most of
the time you will find the ancestor in fewer hops going up from the
element, rather than down from the document. YMMV.
You might be tempted to "bundle" all of these (four!) arguments in a
single options object. Don't. It's just not a good idea at this
(foundation) level. These functions get called often at run time, but
are often wrapped in higher-level functions, so the application
developer is not often typing out the signatures. Creating and
discarding an object on every function call is just a self-imposed
performance penalty. Granted, that technique can make for some really
"cool" looking code. :)
How about preventing the default action?
// NOTE: Does not work in IE 8-, so use only with attachListener
renditions that degrade in IE 8-
function cancelDefault(e) {
e.preventDefault();
}
For IE 8-:-
function cancelDefault(e) {
e.returnValue = false;
}
These are created as companion renditions to the attach/detachListener
functions. If using the forked rendition, put each in the appropriate
"prong".
And that's about it, isn't it? You want to cancel bubbling. No, you
just think you want to do that. It's never a good idea as your widget
(or whatever) does not own the event. Other listeners may very well be
interested in what it has to say.
And speaking of bubbling, not all events can be expected to bubble.
These come to mind as suspect:-
- focus
- blur
- change
- submit
- reset
As for the last two, you'd have to have a pretty crazy design to want
to delegate those. If you have such a design, you need to change it as
there is not a chance in hell in "normalizing" those reliably. You'd
really have to be touched to try.
As for the change event, delegation is highly context-sensitive,
particularly in deciding just when a SELECT should indicate it has
changed. There's three common scenarios and a whole chapter in the
book on that.
It can be useful to delegate focus/blur (e.g. for instant validation
feedback). Though there is not a chance of "normalizing" those either.
As always, you have to think about what you are going to do with the
functions. Once you have the basic focus/blur delegation, you have the
foundation for the most basic change rendition.
Of course, you may never need any of these higher-level wrappers as
you may not need for focus/blur/etc. events to bubble. The one thing
you don't need is to start tangling up the basic attach/detach logic
with "normalization" code.
If using XHR (asynchronous, of course), SQL, Geo Location or anything
else that calls back asynchronously, in conjunction with DOM events, a
common message queue can make it easier to manage the interactions
between the user, their local storage, the server and the document.
You don't need AOR or "custom events", just a simple structure to
queue up and deliver messages to "subscribers". This is how widgets
talk to applications, frameworks and other widgets.
What to expect from the "standard" libraries (e.g. jQuery). What else?
One pair of methods for all types of host objects, complicated event
registries, (in the case of jQuery) no way to set the - this - object
without calling an obscure (and highly amusing) "proxy" method, logic
tangled up in queries (e.g. "Live"), etc. They also tangle up their
basic functions with all manner of confused "normalization" logic,
further muddying the context in which these scripts can be expected to
"work". This is unfortunate as they typically have no fallback plan.
If browsers don't meet *their* expectations, they are just not
responsible for what their script may do to *your* document. You saw
the browser icons. As for the end-users, they are on their own;
anything goes.
And if you are lucky, they may even throw in some browser sniffing. So
you've got that to look forward to. :)
At the very least, you'd expect the libraries to fix the Internet
Explorer Memory Leak Problem. But such expectations are dashed as most
(including jQuery last I checked) create leaks and then try to "fix"
them in an unload listener. Yes, they don't avoid creating leaks, but
just accept that leaks are a given and tack on an unload listener to
"clean up" on navigation. Thanks Crockford. Love the CSS Resets too! :
(
But what if your site is one of those single-page applications that
never navigates? It will continue to leak as you add listeners and, if
it runs long enough, will eventually crash the browser (or at least
the tab).
Hopefully, if you have been paying attention over the years, you
avoided turning your site into a single-page wonder. The entire
chapter on managing browser history reads: don't even think about it
(you idiot).
http://www.cinsoft.net/
http://www.twitter.com/cinsoft
http://jsperf.com/browse/david-mark