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

Discussion in 'Javascript' started by David Mark, Dec 15, 2011.

  1. David Mark

    David Mark Guest

    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
     
    David Mark, Dec 15, 2011
    #1
    1. Advertising

  2. David Mark

    dhtml Guest

    On Dec 14, 5:09 pm, David Mark <> 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
     
    dhtml, Dec 17, 2011
    #2
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. David Mark
    Replies:
    16
    Views:
    940
    Scott Sauyet
    Nov 11, 2011
  2. David Mark
    Replies:
    58
    Views:
    1,539
    David Mark
    Dec 6, 2011
  3. David Mark
    Replies:
    6
    Views:
    772
    Dr J R Stockton
    Nov 16, 2011
  4. David Mark
    Replies:
    1
    Views:
    786
    David Mark
    Dec 7, 2011
  5. David Mark
    Replies:
    8
    Views:
    655
    Thomas 'PointedEars' Lahn
    Dec 10, 2011
Loading...

Share This Page