Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

jQuery.when() progress for array of Deferred and/or Promise

I'm using jQuery's .when() to wrap an array of promises so that I can do some action when all promises have been resolved.

$.when.apply($, requests).done(function () {
    console.log(arguments); //it is an array like object which can be looped
    var total = 0;
    $.each(arguments, function (i, data) {
        console.log(data); //data is the value returned by each of the ajax requests

        total += data[0]; //if the result of the ajax request is a int value then
    });

    console.log(total)
});

Suppose I wanted to be notified when each individual promise was resolved, as a way to show progress. For instance, if requests has 50 requests and 3 of them are resolved, I would like to be able to display a progress bar at 6%. Is there a way to use $.when so that it can return an overall progress without modification of the inside promises and their progress events?

like image 209
Brad Avatar asked Sep 26 '14 18:09

Brad


4 Answers

$.when() doesn't do progress notifications for you. Register for progress notifications on each individual promise or you could make your own version of $.when() that wraps it by first registering for done notifications on each one and then calling $.when().

$.whenWithProgress = function(arrayOfPromises, progessCallback) {
   var cntr = 0;
   for (var i = 0; i < arrayOfPromises.length; i++) {
       arrayOfPromises[i].done(function() {
           progressCallback(++cntr, arrayOfPromises.length);
       });
   }
   return jQuery.when.apply(jQuery, arrayOfPromises);
}

$.whenWithProgress(requests, function(cnt, total) {
    console.log("promise " + cnt + " of " + total + " finished");
}).then(function() {
    // done handler here
}, function() {
    // err handler here
});

I've been thinking about this some more and it bothered me a bit that you want progress notifications, jQuery promises have a progress mechanism and we're not using it. That means progress notifications aren't as extensible as they could be.

Unfortunately, because $.when() returns a promise (not a deferred) and you can't use .notify() on a promise to trigger progress notifications, the above code is the simplest way to do it. But, in the interest of using .progress notifications instead of a custom callback, it could be done like this:

$.whenWithProgress = function(arrayOfPromises) {
   var cntr = 0, defer = $.Deferred();
   for (var i = 0; i < arrayOfPromises.length; i++) {
       arrayOfPromises[i].done(function() {
           defer.notify(++cntr, arrayOfPromises.length);
       });
   }
   // It is kind of an anti-pattern to use our own deferred and 
   // then just resolve it when the promise is resolved
   // But, we can only call .notify() on a defer so if we want to use that, 
   // we are forced to make our own deferred
   jQuery.when.apply(jQuery, arrayOfPromises).done(function() {
       defer.resolveWith(null, arguments);
   });
   return defer.promise();
}

$.whenWithProgress(requests).then(function() {
    // done handler here
}, function() {
    // err handler here
}, function(cnt, total) {
    // progress handler here
    console.log("promise " + cnt + " of " + total + " finished");
});

An argument against this second implementation is that the promise standards effort seems to be moving away from having progress connected to promises in any way (progress would have a separate mechanism). But, it's in jQuery now and probably will be for a long time (jQuery does not follow the promise standards to the letter) so it's really your choice which way to go.

like image 133
jfriend00 Avatar answered Oct 23 '22 06:10

jfriend00


I don't think you can (or should) do this with $.when, whose callback is intended to only be invoked once - you want a callback that can be invoked multiple times, so you can pass it to each promise in requests. For example:

var count = requests.length;
var complete = 0;

function progress(response) {
    complete += 1;
    showProgress(complete / count);
    saveTheResponseSomewhere(response);

    if (complete === count) {
        doSomeAllDoneAction();
    }
}

requests.forEach(function(request) {
    request.then(progress);
});

You can add processing for actual jqXHR progress notifications using the third argument to .then. You probably also need to make sure the results are associated with the appropriate request, which might involve an extra closure in the callback.

like image 27
nrabinowitz Avatar answered Oct 23 '22 06:10

nrabinowitz


Keeping things simple, you can define a generalised classical constructor.

var Progress = function(promiseArray, reporter) {
    this.promiseArray = promiseArray;
    this.reporter = reporter || function(){};
    this.complete = 0;
    $.each(promiseArray, function(i, p) {
        p.then(this.increment).then(this.report);
    });
};
Progress.prototype.increment = function() {
    this.complete += 1;
};
Progress.prototype.report = function() {
    return this.reporter(this.complete, this.promiseArray.length);
};
Progress.prototype.get = function() {
    return { complete:this.complete , total:this.promiseArray.length};
};

Then, for eg a progress thermometer :

var progObj = new Progress(requests, function(complete, total) {
    var scale = 150;
    $("selector").css('someProperty', (scale * complete / total) + 'px');
});

Or for ad hoc enquiry :

console.log( progObj.get() );

The benefit of this approach is reusability. new Progress() could be called on any number of arrays of promises, each with its own reporter callback.

If you wanted, Progress could be made to return a jfriend-style notifiable promise, though I wouldn't do it that way for reasons jfriend has already given.

And with some more thought Progress could be framed as a jQuery plugin, allowing you to call, for example, as follows :

$("selector").progress(function(complete, total) {
    var scale = 150;
    $(this).css('someProperty', (scale * complete / total) + 'px');
});

which may have advantages in some circumstances.

like image 2
Roamer-1888 Avatar answered Oct 23 '22 08:10

Roamer-1888


Try

html

<progress id="progress" min="0" max="100"></progress>
<label for="progress"></label> 

js

$(function () {
    var arrayOfPromises = $.map(new Array(50), function (v, k) {
        return v === undefined ? new $.Deferred(function (dfd) {
            $.post("/echo/json/", {
                json: JSON.stringify(k)
            }).done(function (data, textStatus, jqxhr) {
                return dfd.notify(k).resolve([data, textStatus, jqxhr])
            });
            return dfd.promise()
        }) : null
    }),
        res = [],
        count = null;

    $.each(arrayOfPromises, function (k, v) {
        $.when(v)
            .then(function (p) {
            console.log(p[1]);
            res.push(p);
            if (res.length === arrayOfPromises.length) {
                console.log(res);
                $("label").append(" done!");
            }
        }, function (jqxhr, textStatus, errorThrown) {
            res.push([textStatus, errorThrown, count])
        }, function (msg) {
            ++count;
            count = count;
            console.log(msg, count);
            $("progress").val(count * 2).next().text(count * 2 + "%");
        })
    })
})

jsfiddle http://jsfiddle.net/guest271314/0kyrdtng/


Previous efforts utilizing an alternative approach:

html

<progress id="progress" value="0" max="100"></progress>
<output for="progress"></output>

js

$(function () {
    $.ajaxSetup({
        beforeSend: function (jqxhr, settings) {
            var prog = $("#progress");
            jqxhr.dfd = new $.Deferred();
            jqxhr.dfd.progress(function (data, _state) {
                prog.val(data)                    
                .siblings("output[for=progress]")
                .text(prog.val() + "% " + _state);   
                if (_state === ("resolved" || "rejected")) {
                    prog.val(100);
                    window.clearInterval(s);
                };
            });
            var count = 0.000001;
            jqxhr.counter = function (j) {
                this.dfd.notify(Math.ceil(count), this.state());
                ++count;
                console.log(this.state(), prog.prop("value"));
            };
            var s = setInterval($.proxy(jqxhr.counter, jqxhr, jqxhr), 15);
        }
    });

    $.post("/echo/json/", {
        json: JSON.stringify({
            "defer": new Array(10000)
        })
    })
    .always(function (data, textStatus, jqxhr) {
        console.log(data, jqxhr.state(), $("#progress").val());
    });
})

jsfiddle http://jsfiddle.net/guest271314/N6EgU/

like image 1
guest271314 Avatar answered Oct 23 '22 06:10

guest271314