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?
$.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.
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.
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.
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/
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With