Search code examples
javascriptjquerydeferred

$.when.apply.done returns different results depending on whether the size of the array is one or many


I'm looking for a little advice regarding the best way to execute multiple ajax calls and then combine the results. My problem is that according to the documentation of the when function it will map the resulting argument differently when there are multiple arguments vs just a single argument. https://api.jquery.com/jquery.when/

This resulted in code that looks like the following. The check seems ugly to me and as far as I can tell would need to be done everytime $.when.apply is used. Is there a way to pass in an array of deferreds to the when function so that the output is of a consistent shape?

//Returns an array of promises that are simple ajax get requests
var getEventFramesPromises = self.webServiceAdapter.getEventFrames(distinctEventFramePaths);
$.when.apply($, getEventFramesPromises).then(function () {
            var eventFrames;
            //Now "arguments" will either be an array with three arguments it the length of the getEventFramesPromises ===1
            if (getEventFramesPromises.length === 1) {
                eventFrames = [arguments[0]];
            } else {
            //Or it will be an array of arrays where each item in the array represents a deferred result
                eventFrames = _.map(arguments, function (args) {
                    return args[0];
                });
            }});

Solution

  • jQuery $.when() is probably the worst designed API in the jQuery set. Not only does it return a different data type based on what you pass it, but it also doesn't even accept an array as an argument which is the most common and flexible way to use it. What I'd suggest is a simple wrapper that "fixes" the design to be consistent:

    // Takes an array of promises and always returns an array of results, even if only one result
    $.all = function(promises) {
        if (!Array.isArray(promises)) {
            throw new Error("$.all() must be passed an array of promises");
        }
        return $.when.apply($, promises).then(function () {
            // if single argument was expanded into multiple arguments, then put it back into an array
            // for consistency
            var args = Array.prototype.slice.call(arguments, 0);
            if (promises.length === 1 && arguments.length > 1) {
                // put arguments into an array for consistency
                return [args];
            } else {
                return args;
            }
        });
    };
    

    And, here's a slightly different implementation that also takes the three element array that $.ajax() resolves to and makes it just be the single data value so your resolved value is just a simple array of single element results:

    // jQuery replacement for $.when() that works more like Promise.all()
    // Takes an array of promises and always returns an array of results, even if only one result
    $.all = function (promises) {
        if (!Array.isArray(promises)) {
            throw new Error("$.all() must be passed an array of promises");
        }
        return $.when.apply($, promises).then(function () {
            // if single argument was expanded into multiple arguments, then put it back into an array
            // for consistency
            var args = Array.prototype.slice.call(arguments, 0);
            var returnVal;
            if (promises.length === 1 && arguments.length > 1) {
                // put arguments into an array for consistency
                returnVal = [args];
            } else {
                returnVal = args;
            }
            // now make Ajax results easier to use by making it be just an array of results, not an array of arrays
            // 
            return returnVal.map(function(item) {
                // see if this looks like the array of three values that jQuery.ajax() resolves to
                if (Array.isArray(item) && item.length === 3 && typeof item[2] === "object" && typeof item[2].done === "function") {
                    // just the data
                    return item[0];
                } else {
                    return item;
                }
            });
        });
    };