tags:

views:

392

answers:

5

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?

A: 

I'm assuming you are talking about the event bubbling aspect of it. It would be helpful to see your HTML for the said span elements as well. I can't see why you'd want to change the core behavior like this, I don't find it at all annoying. I suggest going with your second block of code:

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

Most importantly I think you'll find it more organized if you manage all the events for a given element in the same block like you've illustrated. Can you explain why you find this annoying?

Joseph Silvashy
This was just an example to illustrate the issue. My actual case is more complicated, such that to rewrite the code in that form would take some work. In some situations, I could even be using a 3rd party library and may not want to modify it. I just think it would be more convienient to have the option of adding the event to either the beginning *or* the end of the event stack. If there is a way of doing this in jQuery, I would be interested in finding out.
asgeo1
+4  A: 

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'); 
RussellUresti
+1 You learn something new every day. Thank you Russell!
Sean Vieira
Thanks mate. I didn't know about custom namespaces either. I'll definitly be able to use this technique in the future.I gave the correct answer to Anurag, because it was closer to what I was trying to achieve in this circumstance.
asgeo1
A: 

The standard principle is separate event handlers shouldn't depend upon the order they are called. If they do depend upon the order they should not be separate.

Otherwise, you register one event handler as being 'first' and someone else then registers their event handler as 'first' and you're back in the same mess as before.

KeeperOfTheSoul
+4  A: 

A very good question ... I was intrigued so I did a little digging; for those who are interested, here's where I went, and what I came up with.

Looking at the source code for jQuery 1.4.2 I saw this block between lines 2361 and 2392:

jQuery.each(["bind", "one"], function( i, name ) {
    jQuery.fn[ name ] = function( type, data, fn ) {
        // Handle object literals
        if ( typeof type === "object" ) {
            for ( var key in type ) {
                this[ name ](key, data, type[key], fn);
            }
            return this;
        }

        if ( jQuery.isFunction( data ) ) {
            fn = data;
            data = undefined;
        }

        var handler = name === "one" ? jQuery.proxy( fn, function( event ) {
            jQuery( this ).unbind( event, handler );
            return fn.apply( this, arguments );
        }) : fn;

        if ( type === "unload" && name !== "one" ) {
            this.one( type, data, fn );

        } else {
            for ( var i = 0, l = this.length; i < l; i++ ) {
                jQuery.event.add( this[i], type, handler, data );
            }
        }

        return this;
    };
});

There is a lot of interesting stuff going on here, but the part we are interested in is between lines 2384 and 2388:

else {
    for ( var i = 0, l = this.length; i < l; i++ ) {
        jQuery.event.add( this[i], type, handler, data );
    }
}

Every time we call bind() or one() we are actually making a call to jQuery.event.add() ... so let's take a look at that (lines 1557 to 1672, if you are interested)

add: function( elem, types, handler, data ) {
// ... snip ...
        var handleObjIn, handleObj;

        if ( handler.handler ) {
            handleObjIn = handler;
            handler = handleObjIn.handler;
        }

// ... snip ...

        // Init the element's event structure
        var elemData = jQuery.data( elem );

// ... snip ...

        var events = elemData.events = elemData.events || {},
            eventHandle = elemData.handle, eventHandle;

        if ( !eventHandle ) {
            elemData.handle = eventHandle = function() {
                // Handle the second event of a trigger and when
                // an event is called after a page has unloaded
                return typeof jQuery !== "undefined" && !jQuery.event.triggered ?
                    jQuery.event.handle.apply( eventHandle.elem, arguments ) :
                    undefined;
            };
        }

// ... snip ...

        // Handle multiple events separated by a space
        // jQuery(...).bind("mouseover mouseout", fn);
        types = types.split(" ");

        var type, i = 0, namespaces;

        while ( (type = types[ i++ ]) ) {
            handleObj = handleObjIn ?
                jQuery.extend({}, handleObjIn) :
                { handler: handler, data: data };

            // Namespaced event handlers
                    ^
                    |
      // There is is! Even marked with a nice handy comment so you couldn't miss it 
      // (Unless of course you are not looking for it ... as I wasn't)

            if ( type.indexOf(".") > -1 ) {
                namespaces = type.split(".");
                type = namespaces.shift();
                handleObj.namespace = namespaces.slice(0).sort().join(".");

            } else {
                namespaces = [];
                handleObj.namespace = "";
            }

            handleObj.type = type;
            handleObj.guid = handler.guid;

            // Get the current list of functions bound to this event
            var handlers = events[ type ],
                special = jQuery.event.special[ type ] || {};

            // Init the event handler queue
            if ( !handlers ) {
                handlers = events[ type ] = [];

                   // ... snip ...

            }

                  // ... snip ...

            // Add the function to the element's handler list
            handlers.push( handleObj );

            // Keep track of which events have been used, for global triggering
            jQuery.event.global[ type ] = true;
        }

     // ... snip ...
    }

At this point I realized that understanding this was going to take more than 30 minutes ... so I searched Stackoverflow for

jquery get a list of all event handlers bound to an element

and found this answer for iterating over bound events:

//log them to the console (firebug, ie8)
console.dir( $('#someElementId').data('events') );

//or iterate them
jQuery.each($('#someElementId').data('events'), function(i, event){

    jQuery.each(event, function(i, handler){

        console.log( handler.toString() );

    });

});

Testing that in Firefox I see that the events object in the data attribute of every element has a [some_event_name] attribute (click in our case) to which is attatched an array of handler objects, each of which has a guid, a namespace, a type, and a handler. "So", I think, "we should theoretically be able to add objects built in the same manner to the [element].data.events.[some_event_name].push([our_handler_object); ... "

And then I go to finish writing up my findings ... and find a much better answer posted by RusselUresti ... which introduces me to something new that I didn't know about jQuery (even though I was staring it right in the face.)

Which is proof that Stackoverflow is the best question-and-answer site on the internet, at least in my humble opinion.

So I'm posting this for posterity's sake ... and marking it a community wiki, since RussellUresti already answered the question so well.

Sean Vieira
+1 Thanks for all of your analysis Sean. Very helpful
asgeo1
+4  A: 
Anurag
You're right, if .data() is not part of the API it is a risk using it. However, I like your suggestion to encapsulate the use of it in a new jQuery function. At least then if .data() breaks in a later release, you only have to update one function
asgeo1
good point.. this way the changes will be isolated to one function.
Anurag