23

I have two abstracted processes (e.g. managed within js objects using the revealing module pattern that do not expose their internals) that fire custom events when they complete. I want to perform an action when both custom events have fired.

The new Deferred logic in jQuery 1.5 seems like it would be an ideal way to manage this, except that the when() method takes Deferred objects that return a promise() (or normal js objects, but then when() completes immediately instead of waiting, which is useless to me).

Ideally I would like to do something like:

//execute when both customevent1 and customevent2 have been fired
$.when('customevent1 customevent2').done(function(){
  //do something
});

What would be the best way to marry these two techniques?

lonesomeday
  • 233,373
  • 50
  • 316
  • 318
Adam Flynn
  • 949
  • 2
  • 9
  • 21

2 Answers2

40

http://jsfiddle.net/ch47n/

I created a small plugin that creates a new jQuery.fn.when method.

Syntax is:

jQuery( "whatever" ).when( "event1 event2..." ).done( callback );

It uses jQuery.when() extensively internally and ensures that all events have been triggered on all elements in the collection before resolving.


Actual plugin code below:

( function( $ ) {

    $.fn.when = function( events ) {

        var deferred, $element, elemIndex, eventIndex;

        // Get the list of events
        events = events.split( /\s+/g );

        // We will store one deferred per event and per element
        var deferreds = [];

        // For each element
        for( elemIndex = 0; elemIndex < this.length; elemIndex++ ) {
            $element = $( this[ elemIndex ] );
            // For each event
            for ( eventIndex = 0; eventIndex < events.length; eventIndex++ ) {
                // Store a Deferred...
                deferreds.push(( deferred = $.Deferred() ));
                // ... that is resolved when the event is fired on this element
                $element.one( events[ eventIndex ], deferred.resolve );
            }
        }

        // Return a promise resolved once all events fired on all elements
        return $.when.apply( null, deferreds );
    };

} )( jQuery );
Julian Aubourg
  • 11,346
  • 1
  • 29
  • 29
  • 1
    Excellent solution. The multiple internal loops using "each" could pose some performance issues if there were a lot of nodes or events, but I honestly don't see a way to improve on this solution at this time. Thanks for the answer. – Adam Flynn Feb 16 '11 at 14:23
  • 2
    http://jsfiddle.net/FfPXq/6/ makes use of for loops and uses the $.Deferred(fn) signature to create a proper closure to unbind. – Julian Aubourg Feb 17 '11 at 02:54
  • +1 Fabulous! i'll be using something like this for asynchronously loading custom UI controls. Before deferreds came into play managing several tiers of events has been nasty! – Brian May 20 '11 at 20:36
13

You can have the event handlers for "customevent1" and "customevent2" each signal a "Deferred" instance when they fire. You can use "$.when()" then to combine those two into one, and that's where you put the handler to fire only after both custom events have fired.

var df1 = $.Deferred(), df2 = $.Deferred();
$('whatever').bind('customevent1', function() {
  // code code code
  df1.resolve();
}).bind('customevent2', function() {
  // code code code
  df2.resolve();
});

var whenBoth = $.when(df1, df2);

whenBoth.then(function() {
  // code to run after both "customevent1"
  // and "customevent2" have fired
});

Old answer, here for completeness sake

You can make your own Deferred object that keeps track of the two conditions and fires "resolve" when both are set:

function watchEvents() {
  var df = $.Deferred();

  var flags = {};
  $.each(Array.prototype.slice.call(arguments, 0), function() {
    flags[this] = false;
  });

  var realResolve = df.resolve.bind(df);
  df.resolve = function(eventName) {
    flags[eventName] = true;
    for (var ev in flags) if (flags[ev] === false) return;
    realResolve();
  };

  return df;
}

Now you can call that function:

var df = watchEvents("customevent1", "customevent2");

And now your event handlers for those events just need to call "resolve" on that thing when they catch the events:

    df.resolve(event.type);

Each handler reports its own type. Only when all of the event types requested when you called "watchEvents" have happened will the handler functions you register on "df" actually get called.

It occurs to me that another thing you could do would be to write a jQuery plugin that initializes a Deferred object for elements, and stores it in a ".data()" property. You could then write some more plugins that can be used by event handlers to signal themselves, and other plugins to register handlers for multi-event sequences. That'd be pretty cool, I think, but I need to spend some time pondering it.

Pointy
  • 405,095
  • 59
  • 585
  • 614
  • Is "df.reserve" a typo above? I cannot find a reserve property in the jQuery Deferred API documentation. If you could provide a link to it if it exists, I would appreciate it. – Adam Flynn Feb 15 '11 at 21:13
  • Also - there's a better way to do this but I'm juggling a couple of alternative ways of doing it - I'll update shortly. – Pointy Feb 15 '11 at 21:15