David Mark's Essential Javascript Tips - Volume #8 - Tip #47E - Attaching and Detaching Event Listeners

162 views
Skip to first unread message

David Mark

unread,
Dec 14, 2011, 8:09:58 PM12/14/11
to
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

dhtml

unread,
Dec 17, 2011, 3:07:22 AM12/17/11
to
On Dec 14, 5:09 pm, David Mark <dmark.cins...@gmail.com> wrote:
> 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).
>
Where "work" might be defined as doing whatever it was that hte person
coding the app wanted.

> There isn't any practical way to cram all three of these scenarios

A fundamentally important point.

That is where all of the major event libraries fail miserably. They
try to handle each and every scenario, often making endless patches
resulting in generalized functions that are blown way out of
proportion to the context they're used serve.

> 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.
>
Right.

> 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

HTML5 codifies existing browser behavior and adds new features.
Readers should RTFM, not take your word for it.

> 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
>
The problems with that method were also discussed. I think "Should
isHostMethod be added to the FAQ? " covers them. The reader should
investigate those.

[...]

>
> And speaking of bubbling, not all events can be expected to bubble.
> These come to mind as suspect:-
>

Suspiciously mysterious handwaving. Folks, read the w3c specs and
browser documentation (MSDN, MDC) yourselves and test.

> - focus
> - blur
> - change
> - submit
> - reset
>

[...]

>
> 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

ISTM `live` was jQuery's answer to the criticism that the "find
something do something" method was a failure.

> basic functions with all manner of confused "normalization" logic,

Or "decontextualizing" or "centralizing" or "generalizing".

Abstraction isn't bad when you're not too generlized. Context is key.

What I like is to have two interface objects and then use whichever
one suits my needs at the time. One interface object is just method
redispatching, which is useful for event properties of either custom
objects or DOM objects. The other uses an adapter for DOM events or IE
DOM events. The interface object that suits that scenario can be used.

[...]

> Hopefully, if you have been paying attention over the years, you
Optimism!
--
Garrett

Adam Silver

unread,
Mar 14, 2012, 10:04:10 AM3/14/12
to
Thanks David,

We have most of this on the Jessie project now.

David Mark

unread,
Mar 14, 2012, 4:41:17 PM3/14/12
to
On Mar 14, 10:04 am, Adam Silver <adambsil...@gmail.com> wrote:
> Thanks David,
>
> We have most of this on the Jessie project now.
>

Good deal. For those who don't know, the Jessie project is a
libraryspace. For those who don't know what that means, you haven't
been following my Twitter account (and shame on you). :)

Andrew Poulos

unread,
Mar 15, 2012, 12:36:40 AM3/15/12
to
I've no idea what a "libraryspace" is :-(

Where can I found out more about the Jessie project (Google appears
unhelpful in this regard)?

Andrew Poulos

David Mark

unread,
Mar 15, 2012, 1:37:55 AM3/15/12
to
On Mar 15, 12:36 am, Andrew Poulos <ap_p...@hotmail.com> wrote:
> On 15/03/2012 7:41 AM, David Mark wrote:
>
> > On Mar 14, 10:04 am, Adam Silver<adambsil...@gmail.com>  wrote:
> >> Thanks David,
>
> >> We have most of this on the Jessie project now.
>
> > Good deal.  For those who don't know, the Jessie project is a
> > libraryspace.  For those who don't know what that means, you haven't
> > been following my Twitter account (and shame on you). :)
>
> I've no idea what a "libraryspace" is :-(

It's a silly word I made up for Richard's suggestion (from four years
ago) about a repository of interchangeable functions that could be
used to build context-specific libraries. It's what the CWR project
was before it was abandoned. I ended up turning my contributions into
My Library. As mentioned at the time, a library (modular or not) was
not what I wanted to do (but did anyway, just for the hell of it).

>
> Where can I found out more about the Jessie project (Google appears
> unhelpful in this regard)?

It's on Git. Basically, contributors are gathering up my examples from
this series of tips and JSPerf at the moment. Ultimately, a builder
(perhaps using node.js) will stitch the functions together to fit
specified contexts. Not that you necessarily need a builder, assuming
you have a clipboard. :)

One key concept is that you build your library at the design stage, so
you develop and test against the same code that will ultimately be
released. Contrast with something like Google Closure where you
develop/test with a huge mess and then try to filter out all of the
unneeded bits at the last second. Also, their "compiler" is not
context-specific in the same sense.

Another, which is more important today than back in 2007, is that it
allows the developer to decide on degradation points for the old
MSHTML-based browsers (i.e. is the client okay with a static
presentation for IE 6/7?) Typically, function renditions that fork for
the old IE browsers are much larger than those that bail out in those
environments. Contrast with monolithic GP libraries that send tons of
IE 6/7 hacks to iPhones until the developers either:

A. Stop "caring" about IE 6/7, at which time their sites
unceremoniously break for corporate users stuck with those browsers
B. Use client side browser sniffing to dynamically load browser-
specific scripts (!)
C. Give up and build a second site for mobile devices, which requires
server side browser sniffing (!!)

None of those is appealing if you are thinking straight, but C is the
worst in terms of wasted effort and the potential to piss off
visitors. I've always hated being redirected to alternate URI's by
browser sniffers and an informal poll I conducted recently indicated
that most end-users "hate" (their words) mobile sites. When asked why,
the number one answer was: because they don't work. The "don't work"
sentiment is subject to a number of interpretations. I know I hate it
when I click a link to a specific news story and end up on the home
page of a mobile site. Similarly, it's irritating when these sites try
to imitate mobile OS's, stepping on expected browser behavior to get
it "just right". Often they don't scale, crop overflowed content
indiscriminately (e.g. html { overflow: hidden }), scroll poorly, etc.
Just what you want on a tiny screen, right? And, of course, they often
include huge scripts and style sheets to make all of this happen. :(

Clearly the answer is "none of the above" (and it always has been). ;)

If you've read this series of tips (or were around when the CWR
project was active), you've got the basics. Follow and/or contribute
to the project to learn more.

https://github.com/rassie/jessie
https://twitter.com/Cinsoft

Norman Peelman

unread,
Mar 15, 2012, 6:38:46 AM3/15/12
to
It's 2012 and we still can't get hardware manufacturers to id stuff
with proper mobile user-agents (if anyone wants to fake their user-agent
then that's their problem). I prefer a mobile version over pages that
scroll left and right, making me scroll around and double-tapping all
over trying to see what I want to see. Problem is, the mobile versions
need to be stripped down, no floatsam and jetsam. That's what I expect
to see, not a bunch of (flash/Flash) ads that I obviously don't have the
screen real estate for (but they gotta get those ads displayed!) You say
this isn't acceptable but yet rather than design mobile pages, companies
now create 'apps' that accomplish the same thing. There again, I
shouldn't need to have an app for every site I like to visit.


--
Norman
Registered Linux user #461062
AMD64X2 6400+ Ubuntu 10.04 64bit

David Mark

unread,
Mar 15, 2012, 8:38:08 AM3/15/12
to
UA strings lie without any help from the users (and you can't make
inferences about features based on such a string anyway). Users "fake"
their UA string to counter incompetently designed Websites. It's an
endless cycle of espionage/counterespionage and there's never going to
be any end to it until Web developers finally figure out they are
chasing ghosts.

http://jibbering.com/faq/notes/detect-browser/

> I prefer a mobile version over pages that
> scroll left and right, making me scroll around and double-tapping all
> over trying to see what I want to see.

In other words, you prefer mobile versions over incompetently designed
Websites (granted, that's most of them).

> Problem is, the mobile versions
> need to be stripped down, no floatsam and jetsam.

Only if they were bloated beyond belief to start with. ;)

> That's what I expect
> to see, not a bunch of (flash/Flash) ads that I obviously don't have the
> screen real estate for (but they gotta get those ads displayed!)

Flash ads on what mobile device? Regardless, Flash ads are a problem
for everybody, not just mobile users (and they are easy enough to hide
on mobile devices without resorting to browser sniffing).

> You say
> this isn't acceptable but yet rather than design mobile pages, companies
> now create 'apps' that accomplish the same thing.

You have it the wrong way around. Just as some Websites try to mimic
desktop applications, these so-called mobile sites try to mimic mobile
applications. It's always been a bad idea, but bad ideas often become
popular on the Web.

> There again, I
> shouldn't need to have an app for every site I like to visit.
>

Of course not; apps and websites are apples and oranges.
Reply all
Reply to author
Forward
0 new messages