Tuesday, April 17, 2012

jQuery event handlers always execute in order they were bound - any way around this?


It can be anoying that jQuery event handlers always execute in the order they were bound. For example:




$('span').click(doStuff1());
$('span').click(doStuff2());



clicking on the span will cause doStuff1() to fire, followed by doStuff2().



At the time I bind doStuff2(), I would like the option to bind it before doStuff1(), but there doesn't appear to be any easy way to do this.



I suppose most people would say, just write the code like this:




$('span').click(function (){
doStuff2();
doStuff1();
});



But this is just a simple example - in practise it is not always convienient to do that.



There are situations when you want to bind an event, and the object you are binding to already has events. And in this case you may simply want the new event to fire before any other existing events.



So what is the best way to achieve this in jQuery?


Source: Tips4all

2 comments:

  1. As @Sean has discovered, jQuery exposes all event handlers through an element's data interface. Specifically element.data('events'). Using this you could always write a simple plugin whereby you could insert any event handler at a specific position.

    Here's a simple plugin that does just that to insert a handler at the beginning of the list. You can easily extend this to insert an item at any given position. It's just array manipulation. But since I haven't seen jQuery's source and don't want to miss out on any jQuery magic from happening, I normally add the handler using bind first, and then reshuffle the array.

    // [name] is the name of the event "click", "mouseover", ..
    // same as you'd pass it to bind()
    // [fn] is the handler function
    $.fn.bindFirst = function(name, fn) {
    // bind as you normally would
    // don't want to miss out on any jQuery magic
    this.bind(name, fn);

    // Thanks to a comment by @Martin, adding support for
    // namespaced events too.
    var handlers = this.data('events')[name.split('.')[0]];
    // take out the handler we just inserted from the end
    var handler = handlers.pop();
    // move it at the beginning
    handlers.splice(0, 0, handler);
    };


    So for example, for this markup it would work as (example here):

    <div id="me">..</div>

    $("#me").click(function() { alert("1"); });
    $("#me").click(function() { alert("2"); });
    $("#me").bindFirst('click', function() { alert("3"); });

    $("#me").click(); // alerts - 3, then 1, then 2


    However, since .data('events') is not part of their public API as far as I know, an update to jQuery could break your code if the underlying representation of attached events changes from an array to something else, for example.

    Disclaimer: Since anything is possible :), here's your solution, but I would still err on the side of refactoring your existing code, as just trying to remember the order in which these items were attached can soon get out of hand as you keep adding more and more of these ordered events.

    ReplyDelete
  2. You can do a custom namespace of events.

    $('span').bind('click.doStuff1',function(){doStuff1();});
    $('span').bind('click.doStuff2',function(){doStuff2();});


    Then, when you need to trigger them you can choose the order.

    $('span').trigger('click.doStuff1').trigger('click.doStuff2');


    or

    $('span').trigger('click.doStuff2').trigger('click.doStuff1');


    Also, just triggering click SHOULD trigger both in the order they were bound... so you can still do

    $('span').trigger('click');

    ReplyDelete