Here's an contrived example of what's going on: http://jsfiddle.net/adamjford/YNGcm/20/

HTML:

<a href="#">Click me!</a>
<div></div>

JavaScript:

function getSomeDeferredStuff() {
    var deferreds = [];

    var i = 1;
    for (i = 1; i <= 10; i++) {
        var count = i;

        deferreds.push(
        $.post('/echo/html/', {
            html: "<p>Task #" + count + " complete.",
            delay: count
        }).success(function(data) {
            $("div").append(data);
        }));
    }

    return deferreds;
}

$(function() {
    $("a").click(function() {
        var deferreds = getSomeDeferredStuff();

        $.when(deferreds).done(function() {
            $("div").append("<p>All done!</p>");
        });
    });
});

I want "All done!" to appear after all of the deferred tasks have completed, but $.when() doesn't appear to know how to handle an array of Deferred objects. "All done!" is happening first because the array is not a Deferred object, so jQuery goes ahead and assumes it's just done.

I know one could pass the objects into the function like $.when(deferred1, deferred2, ..., deferredX) but it's unknown how many Deferred objects there will be at execution in the actual problem I'm trying to solve.


Solution 1:

To pass an array of values to any function that normally expects them to be separate parameters, use Function.prototype.apply, so in this case you need:

$.when.apply($, my_array).then( ___ );

See http://jsfiddle.net/YNGcm/21/

In ES6, you can use the ... spread operator instead:

$.when(...my_array).then( ___ );

In either case, since it's unlikely that you'll known in advance how many formal parameters the .then handler will require, that handler would need to process the arguments array in order to retrieve the result of each promise.

Solution 2:

The workarounds above (thanks!) don't properly address the problem of getting back the objects provided to the deferred's resolve() method because jQuery calls the done() and fail() callbacks with individual parameters, not an array. That means we have to use the arguments pseudo-array to get all the resolved/rejected objects returned by the array of deferreds, which is ugly:

$.when.apply($,deferreds).then(function() {
     var objects = arguments; // The array of resolved objects as a pseudo-array
     ...
};

Since we passed in an array of deferreds, it would be nice to get back an array of results. It would also be nice to get back an actual array instead of a pseudo-array so we can use methods like Array.sort().

Here is a solution inspired by when.js's when.all() method that addresses these problems:

// Put somewhere in your scripting environment
if (typeof jQuery.when.all === 'undefined') {
    jQuery.when.all = function (deferreds) {
        return $.Deferred(function (def) {
            $.when.apply(jQuery, deferreds).then(
            // the calling function will receive an array of length N, where N is the number of
            // deferred objects passed to when.all that succeeded. each element in that array will
            // itself be an array of 3 objects, corresponding to the arguments passed to jqXHR.done:
            // ( data, textStatus, jqXHR )
            function () {
                var arrayThis, arrayArguments;

                if (Array.isArray(this)) {
                    arrayThis = this;
                    arrayArguments = arguments;
                }
                else {
                    arrayThis = [this];
                    arrayArguments = [arguments];
                }

                def.resolveWith(arrayThis, [Array.prototype.slice.call(arrayArguments)]);
            },
            // the calling function will receive an array of length N, where N is the number of
            // deferred objects passed to when.all that failed. each element in that array will
            // itself be an array of 3 objects, corresponding to the arguments passed to jqXHR.fail:
            // ( jqXHR, textStatus, errorThrown )
            function () {
                var arrayThis, arrayArguments;

                if (Array.isArray(this)) {
                    arrayThis = this;
                    arrayArguments = arguments;
                }
                else {
                    arrayThis = [this];
                    arrayArguments = [arguments];
                }

                def.rejectWith(arrayThis, [Array.prototype.slice.call(arrayArguments)]);
            });
        });
    }
}

Now you can simply pass in an array of deferreds/promises and get back an array of resolved/rejected objects in your callback, like so:

$.when.all(deferreds).then(function(objects) {
    console.log("Resolved objects:", objects);
});

Solution 3:

You can apply the when method to your array:

var arr = [ /* Deferred objects */ ];

$.when.apply($, arr);

How do you work with an array of jQuery Deferreds?