I have this class:
(function(){
"use strict";
var FileRead = function() {
this.init();
};
p.read = function(file) {
var fileReader = new FileReader();
var deferred = $.Deferred();
fileReader.onload = function(event) {
deferred.resolve(event.target.result);
};
fileReader.onerror = function() {
deferred.reject(this);
};
fileReader.readAsDataURL(file);
return deferred.promise();
};
lx.FileRead = FileRead;
}(window));
The class is called in a loop:
var self = this;
$.each(files, function(index, file){
self.fileRead.read(file).done(function(fileB64){self.fileShow(file, fileB64, fileTemplate);});
});
My question is, is there a way to call a method once the loop has completed and self.fileRead has returned it's deferred for everything in the loop?
I want it to call the method even if one or more of the deferred fails.
$.when lets you wrap up multiple promises into one. Other promise libraries have something similar. Build up an array of promises returned by fileRead.read and then pass that array to $.when and hook up then/done/fail/always methods to the promise returned by .when
// use map instead of each and put that inside a $.when call
$.when.apply(null, $.map(files, function(index, file){
// return the resulting promise
return self.fileRead.read(file).done(function(fileB64){self.fileShow(file, fileB64, fileTemplate);});
}).done(function() {
//now everything is done
})
var self = this;
var processFiles = function (data) {
var promises = [];
$.each(files, function (index, file) {
var def = data.fileRead.read(file);
promises.push(def);
});
return $.when.apply(undefined, promises).promise();
}
self.processFiles(self).done(function(results){
//do stuff
});
$.when says "when all these promises are resolved... do something". It takes an infinite (variable) number of parameters. In this case, you have an array of promises;
I know this is closed but as the doc states for $.when: In the multiple-Deferreds case where one of the Deferreds is rejected, jQuery.when immediately fires the failCallbacks for its master Deferred. (emphasis on immediately is mine)
If you want to complete all Deferreds even when one fails, I believe you need to come up with your own plugin along those lines below. The $.whenComplete function expects an array of functions that return a JQueryPromise.
var whenComplete = function (promiseFns) {
var me = this;
return $.Deferred(function (dfd) {
if (promiseFns.length === 0) {
dfd.resolve([]);
} else {
var numPromises = promiseFns.length;
var failed = false;
var args;
var resolves = [];
promiseFns.forEach(function (promiseFn) {
try {
promiseFn().fail(function () {
failed = true;
args = arguments;
}).done(function () {
resolves.push(arguments);
}).always(function () {
if (--numPromises === 0) {
if (failed) {
//Reject with the last error
dfd.reject.apply(me, args);
} else {
dfd.resolve(resolves);
}
}
});
} catch (e) {
var msg = 'Unexpected error processing promise. ' + e.message;
console.error('APP> ' + msg, promiseFn);
dfd.reject.call(me, msg, promiseFn);
}
});
}
}).promise();
};
To address the requirement, "to call the method even if one or more of the deferred fails" you ideally want an .allSettled() method but jQuery doesn't have that particular grain of sugar, so you have to do a DIY job :
You could find/write a $.allSettled() utility or achieve the same effect with a combination of .when() and .then() as follows :
var self = this;
$.when.apply(null, $.map(files, function(index, file) {
return self.fileRead.read(file).then(function(fileB64) {
self.fileShow(file, fileB64, fileTemplate);
return fileB64;//or similar
}, function() {
return $.when();//or similar
});
})).done(myMethod);
If it existed, $.allSettled() would do something similar internally.
Next, "in myMethod, how to distinguish the good responses from the errors?", but that's another question :)
Related
I want to write the unit test for the factory which have lot chain of promises. Below is my code snippet:
angular.module('myServices',[])
.factory( "myService",
['$q','someOtherService1', 'someOtherService2', 'someOtherService3', 'someOtherService4',
function($q, someOtherService1, someOtherService2, someOtherService3, someOtherService4) {
method1{
method2().then(
function(){ someOtherService3.method3();},
function(error){/*log error;*/}
);
return true;
};
var method2 = function(){
var defer = $q.defer();
var chainPromise = null;
angular.forEach(myObject,function(value, key){
if(chainPromise){
chainPromise = chainPromise.then(
function(){return method4(key, value.data);},
function(error){/*log error*/});
}else{
chainPromise = method4(key, value.data);
}
});
chainPromise.then(
function(){defer.resolve();},
function(error){defer.reject(error);}
);
return defer.promise;
};
function method4(arg1, arg2){
var defer = $q.defer();
someOtherService4.method5(
function(data) {defer.resolve();},
function(error) {defer.reject(error);},
[arg1,arg2]
);
return defer.promise;
};
var method6 = function(){
method1();
};
return{
method6:method6,
method4:method4
};
}]);
To test it, I have created spy object for all the services, but mentioning the problematic one
beforeEach( function() {
someOtherService4Spy = jasmine.createSpyObj('someOtherService4', ['method4']);
someOtherService4Spy.method4.andCallFake(
function(successCallback, errorCallback, data) {
// var deferred = $q.defer();
var error = function (errorCallback) { return error;}
var success = function (successCallback) {
deferred.resolve();
return success;
}
return { success: success, error: error};
}
);
module(function($provide) {
$provide.value('someOtherService4', someOtherService4);
});
inject( function(_myService_, $injector, _$rootScope_,_$q_){
myService = _myService_;
$q = _$q_;
$rootScope = _$rootScope_;
deferred = _$q_.defer();
});
});
it("test method6", function() {
myService.method6();
var expected = expected;
$rootScope.$digest();
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});
It is showing error on
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
After debugging I found that it is not waiting for any promise to resolve, so method 1 return true, without even executing method3. I even tried with
someOtherService4Spy.method4.andReturn(function(){return deferred.promise;});
But result remain same.
My question is do I need to resolve multiple times ie for each promise. How can I wait till all the promises are executed.
method1 does not return the promise so how would you know the asynchrounous functions it calls are finished. Instead you should return:
return method2().then(
method6 calls asynchronous functions but again does not return a promise (it returns undefined) so how do you know it is finished? You should return:
return method1();
In a test you should mock $q and have it resolve or reject to a value but I can't think of a reason why you would have a asynchronous function that doesn't return anything since you won't know if it failed and when it's done.
Method 2 could be written in a more stable way because it would currently crash if the magically appearing myObject is empty (either {} or []
var method2 = function(){
var defer = $q.defer();
var keys = Object.keys(myObject);
return keys.reduce(
function(acc,item,index){
return acc.then(
function(){return method4(keys[index],myObject[key].data);},
function(err){console.log("error calling method4:",err,key,myObject[key]);}
)
}
,$q.defer().resolve()
)
};
And try not to have magically appearing variables in your function, this could be a global variable but your code does not show where it comes from and I doubt there is a need for this to be scoped outside your function(s) instead of passed to the function(s).
You can learn more about promises here you should understand why a function returns a promise (functions not block) and how the handlers are put on the queue. This would save you a lot of trouble in the future.
I did below modification to get it working. I was missing the handling of request to method5 due to which it was in hang state. Once I handled all the request to method 5 and provided successCallback (alongwith call to digest()), it started working.
someOtherService4Spy.responseArray = {};
someOtherService4Spy.requests = [];
someOtherService4Spy.Method4.andCallFake( function(successCallback, errorCallback, data){
var request = {data:data, successCallback: successCallback, errorCallback: errorCallback};
someOtherService4Spy.requests.push(request);
var error = function(errorCallback) {
request.errorCallback = errorCallback;
}
var success = function(successCallback) {
request.successCallback = successCallback;
return {error: error};
}
return { success: success, error: error};
});
someOtherService4Spy.flush = function() {
while(someOtherService4Spy.requests.length > 0) {
var cachedRequests = someOtherService4Spy.requests;
someOtherService4Spy.requests = [];
cachedRequests.forEach(function (request) {
if (someOtherService4Spy.responseArray[request.data[1]]) {
request.successCallback(someOtherService4Spy.responseArray[request.data[1]]);
} else {
request.errorCallback(undefined);
}
$rootScope.$digest();
});
}
}
Then I modified my test as :
it("test method6", function() {
myService.method6();
var expected = expected;
var dataDict = {data1:"data1", data2:"data2"};
for (var data in dataDict) {
if (dataDict.hasOwnProperty(data)) {
someOtherService4Spy.responseArray[dataDict[data]] = dataDict[data];
}
}
someOtherService4Spy.flush();
expect(someOtherService3.method3.mostRecentCall.args[0]).toEqualXml(expected);
expect(someOtherService4Spy.method4).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function), [arg,arg]);
});
This worked as per my expectation. I was thinking that issue due to chain of promises but when I handled the method5 callback method, it got resolved. I got the idea of flushing of requests as similar thing I was doing for http calls.
I have a simple service method that gather several .get() and I'm having some troubles on the "printing" part as by that time I only have one part of the result.
what I'm doing is:
var service = function() {
var players = []; // will hold 100 objects
var getMembers = function(id) {
$.get(url, function(data) {
for(i=0; i<data.length; i++) {
var p = data[i];
// get more info for this member
getMemberDetails(p.member_id);
// put the current data into the players
players.push(p);
}
});
calculateAndPrint();
};
var getMemberDetails = function(id) {
$.get(url, function(data) {
var result = $.grep(players, function(e){ return e.member_id == id; });
if (result.length == 0) { /* not found */ }
else if (result.length == 1) {
// push new data to player object
result[0].details = data;
}
});
};
var calculateAndPrint = function() {
for(i=0; i<players.length; i++) {
var p = players[i];
console.log(p);
}
};
})();
and this does not work, as when I reach calculateAndPrint, the details is not even designed yet...
so I tried $.Deferred() and the only issue I'm having is that if I defer getMemberDetails method, that call includes already a deffer call (the .get()) and I'm back to the same issue ...
what is the best option to only run calculateAndPrint after all 100 calls were made?
It seems easy enough but I'm just blank :/
This should be pretty easy if you use promises:
var service = function() {
var getMembers = function(id) {
return Promise.when($.get("some service url"))
.then(function (data) {
return Promise.all(data.map(getMemberDetails));
});
};
var getMemberDetails = function(player) {
return Promise.when($.get("some service Url?id=" + player.member_id));
};
var calculateAndPrint = function(players) {
players.forEach(function (player) {
console.log(player);
});
};
return {
getMembers: getMembers,
calculateAndPrint: calculateAndPrint
};
})();
service.getMembers().then(function (players) {
service.calculateAndPrint(players);
});
you could just create a deferred object $.deferred for every ajax call your making & then wait ($.when) for all those deferred jobs to complete before you run the calculateAndPrint() method.
How It Works:
Create a deferred object for every ajax call $.deferred & return the promise object .promise().
depending on whether ajax call is successful or not , either resolve with response data .resolve(responseData) or reject with error data .reject(errorData).
Monitor all the ajax calls by there promise objects returned from step1 and on completion , call the calculateAndPrint() method.
For arbitrary ajax calls most of the above logic remains same,except that those are called in for loop and each of those deferred calls are pushed into a deferredCalls array.
Note:I would suggest to show some loader/spinner image when your making ajax calls, since you would not get the response immediately & keeping user informed about the background operation is always good User experience sign.
JS CODE:
/* utils */
var $ul = $('ul');
function msg(text) {
$ul.append('<li>' + text + '</li>');
}
/* functions */
function asyncThing1() {
var dfd = $.Deferred();
setTimeout(function() {
msg('asyncThing1 seems to be done...');
dfd.resolve('banana');
}, 1000);
return dfd.promise();
}
function asyncThing2() {
var dfd = $.Deferred();
setTimeout(function() {
msg('asyncThing2 seems to be done...');
dfd.resolve('apple');
}, 500);
return dfd.promise();
}
function asyncThing3() {
var dfd = $.Deferred();
setTimeout(function() {
msg('asyncThing3 seems to be done...');
dfd.resolve('orange');
}, 1500);
return dfd.promise();
}
/* do it */
$.when(asyncThing1(), asyncThing2(), asyncThing3()).done(function(res1, res2, res3) {
msg('all done!');
msg(res1 + ', ' + res2 + ', ' + res3);
});
Live Demo # JSFiddle
Arbitrary Deferred calls Original SO Post :
//Push all arbitrary ajax calls to deferred array
var deferreds = [];
function getSomeDeferredStuff() {
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);
}));
}
}
// define a extension method for $.when for creating/managing deferred
// objects for every ajax call
if (jQuery.when.all===undefined) {
jQuery.when.all = function(deferreds) {
var deferred = new jQuery.Deferred();
$.when.apply(jQuery, deferreds).then(
function() {
var deferredObjs= function (arguments) { return deferreds.length > 1 ? $.makeArray(arguments) : [arguments]; }
deferred.resolve(deferredObjs);
},
function() {
deferred.fail(deferredObjs);
});
return deferred;
}
}
//passing the deferred calls array to $.when
$.when.all(deferreds).then(function(objects) {
//process when all deferred objects compelted
console.log("Resolved/rejected objects:", objects);
});
Working example for arbitrary ajax calls #JSFiddle
Basically I want this:
function do_ajax_calls(...){
var d = $.Deferred();
$.ajax(args).done(function(){
$.ajax(args).done(function(){
$.ajax(args).done(function(){
d.resolve();
});
});
})
return d.promise();
}
But the number of ajax calls depends on the arguments that I pass to the function, which is an array, so I can't use that code.
The function should return a promise that only resolves when the last ajax calls completes. So the function needs to be called like this:
do_ajax_calls(....).done(function(){
// here is the callback
})
Does anyone know how can I do this?
But the number of ajax calls depends on the arguments that I pass to the function, which is an array
If it's one ajax call per array item
function do_ajax_calls(args) {
return args.reduce(function(promise, item) {
return promise.then(function() {
return $.ajax(args); // should that be item?
});
}, Promise.resolve(true));
}
The Promise.resolve(true) is a "native" promise, i.e. not available in IE, but I'm sure jQuery has an equivalent
Here's a JSFiddle Demo
One of the reasons promises are a big deal is because they can be chained. You can use this to your advantage to iteratively chain additional requests onto the resolution of the previous one:
function do_ajax_calls() {
var dfd = $.Deferred();
var promise = dfd.promise();
var responses = [];
function chainRequest(url) {
promise = promise.then(function (response) {
responses.push(response);
return $.ajax(url, { method: 'POST' });
});
}
for (var i = 0, length = arguments.length; i < length; i++) {
chainRequest(arguments[i]);
}
dfd.resolve();
return promise.then(function (response) {
return responses.slice(1).concat(response);
});
}
The above code will return a promise ultimately resolving to an array of all of the responses. If any one of the requests fails, the promise will reject with the first failure.
JSFiddle
Here is it Demo
var counter = 1 ;
function multipleAjax(loop)
{
if(counter<loop)
{
$.ajax(
{
url: 'http://mouadhhsoumi.tk/echo.php',
success:function(data)
{
multipleAjax(loop);
$(".yo").append(data+"</br>");
counter++;
}
});
}
}
multipleAjax(5);
Try using $.when() , Function.prototype.apply() , $.map()
function do_ajax_calls(args) {
return $.when.apply($, $.map(args, function(request, i) {
return $.ajax(request) // `request` : item with `args` array
}))
}
do_ajax_calls
.then(function success() {
console.log(arguments)
}, function err() {
console.log("err", arguments)
});
Here's an contrived example of what's going on: http://jsfiddle.net/adamjford/YNGcm/20/
HTML:
Click me!
<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.
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.
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);
});
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?
When calling multiple parallel AJAX calls, you have two options for handling the respective responses.
Use Synchronous AJAX call/ one after another/ not recommended
Use Promises' array and $.when which accepts promises and its callback .done gets called when all the promises are return successfully with respective responses.
Example
function ajaxRequest(capitalCity) {
return $.ajax({
url: 'https://restcountries.eu/rest/v1/capital/'+capitalCity,
success: function(response) {
},
error: function(response) {
console.log("Error")
}
});
}
$(function(){
var capitalCities = ['Delhi', 'Beijing', 'Washington', 'Tokyo', 'London'];
$('#capitals').text(capitalCities);
function getCountryCapitals(){ //do multiple parallel ajax requests
var promises = [];
for(var i=0,l=capitalCities.length; i<l; i++){
var promise = ajaxRequest(capitalCities[i]);
promises.push(promise);
}
$.when.apply($, promises)
.done(fillCountryCapitals);
}
function fillCountryCapitals(){
var countries = [];
var responses = arguments;
for(i in responses){
console.dir(responses[i]);
countries.push(responses[i][0][0].nativeName)
}
$('#countries').text(countries);
}
getCountryCapitals()
})
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<h4>Capital Cities : </h4> <span id="capitals"></span>
<h4>Respective Country's Native Names : </h4> <span id="countries"></span>
</div>
As a simple alternative, that does not require $.when.apply or an array, you can use the following pattern to generate a single promise for multiple parallel promises:
promise = $.when(promise, anotherPromise);
e.g.
function GetSomeDeferredStuff() {
// Start with an empty resolved promise (or undefined does the same!)
var promise;
var i = 1;
for (i = 1; i <= 5; i++) {
var count = i;
promise = $.when(promise,
$.ajax({
type: "POST",
url: '/echo/html/',
data: {
html: "<p>Task #" + count + " complete.",
delay: count / 2
},
success: function (data) {
$("div").append(data);
}
}));
}
return promise;
}
$(function () {
$("a").click(function () {
var promise = GetSomeDeferredStuff();
promise.then(function () {
$("div").append("<p>All done!</p>");
});
});
});
Notes:
I figured this one out after seeing someone chain promises sequentially, using promise = promise.then(newpromise)
The downside is it creates extra promise objects behind the scenes and any parameters passed at the end are not very useful (as they are nested inside additional objects). For what you want though it is short and simple.
The upside is it requires no array or array management.
I want to propose other one with using $.each:
We may to declare ajax function like:
function ajaxFn(someData) {
this.someData = someData;
var that = this;
return function () {
var promise = $.Deferred();
$.ajax({
method: "POST",
url: "url",
data: that.someData,
success: function(data) {
promise.resolve(data);
},
error: function(data) {
promise.reject(data);
}
})
return promise;
}
}
Part of code where we creating array of functions with ajax to send:
var arrayOfFn = [];
for (var i = 0; i < someDataArray.length; i++) {
var ajaxFnForArray = new ajaxFn(someDataArray[i]);
arrayOfFn.push(ajaxFnForArray);
}
And calling functions with sending ajax:
$.when(
$.each(arrayOfFn, function(index, value) {
value.call()
})
).then(function() {
alert("Cheer!");
}
)
If you're transpiling and have access to ES6, you can use spread syntax which specifically applies each iterable item of an object as a discrete argument, just the way $.when() needs it.
$.when(...deferreds).done(() => {
// do stuff
});
MDN Link - Spread Syntax
I had a case very similar where I was posting in an each loop and then setting the html markup in some fields from numbers received from the ajax. I then needed to do a sum of the (now-updated) values of these fields and place in a total field.
Thus the problem was that I was trying to do a sum on all of the numbers but no data had arrived back yet from the async ajax calls. I needed to complete this functionality in a few functions to be able to reuse the code. My outer function awaits the data before I then go and do some stuff with the fully updated DOM.
// 1st
function Outer() {
var deferreds = GetAllData();
$.when.apply($, deferreds).done(function () {
// now you can do whatever you want with the updated page
});
}
// 2nd
function GetAllData() {
var deferreds = [];
$('.calculatedField').each(function (data) {
deferreds.push(GetIndividualData($(this)));
});
return deferreds;
}
// 3rd
function GetIndividualData(item) {
var def = new $.Deferred();
$.post('#Url.Action("GetData")', function (data) {
item.html(data.valueFromAjax);
def.resolve(data);
});
return def;
}
If you're using angularJS or some variant of the Q promise library, then you have a .all() method that solves this exact problem.
var savePromises = [];
angular.forEach(models, function(model){
savePromises.push(
model.saveToServer()
)
});
$q.all(savePromises).then(
function success(results){...},
function failed(results){...}
);
see the full API:
https://github.com/kriskowal/q/wiki/API-Reference#promiseall
https://docs.angularjs.org/api/ng/service/$q
I'm trying to sync up multiple ajax callbacks using jQuery.Deferrd objects. Obviously jQuery.when handles this for you however my code is architected in such a way that the ajax requests aren't called in the same method. So for example this is the flow:
// A Button is clicked
// Module 1 requests a snippet of html and updates the DOM
// Module 2 requests a different snippet of html and updates the DOM
I need both Modules to update the DOM at the same time meaning I need to ensure the callbacks are run after both requests have returned.
Module 1 and Module 2 need to be able to exist without each other and should have no knowledge of one another so the requests can't be made together using $.when(doMod1Request(), doMod2Request()).then(function () { ... }) and the callbacks should be independent too.
I've therefore written a wrapper around ajax which adds the callbacks to a deferred object and in a similar way to $.when resolves the deferred object once the ajax requests have returned the same number of times as the number of callbacks on the deferred object.
My dilemma is however deferred.resolve() can only be called with one set of arguments so each callback get's the same value.
e.g.
var deferred = new $.Deferred();
deferred.done(function (response) {
console.log(response); // <div class="html-snippet-1"></div>
});
deferred.done(function (response) {
console.log(response); // <div class="html-snippet-1"></div>
});
deferred.resolve('<div class="html-snippet-1"></div>');
Whereas I'd want something like this:
var deferred = new $.Deferred();
deferred.done(function (response) {
console.log(response); // <div class="html-snippet-1"></div>
});
deferred.done(function (response) {
console.log(response); // <div class="html-snippet-2"></div>
});
deferred.resolve(['<div class="html-snippet-1"></div>', '<div class="html-snippet-2"></div>']);
Is this possible or am I going about this incorrectly?
I'd say this is perfectly valid. Assuming your independent modules, you would do (with two Promises):
doMod1Request().done(doMod1Update);
doMod2Request().done(doMod2Update);
Now, if you want to to execute the updates together and only if the two requests both succeeded, just write
$.when(doMod1Request(), doMod2Request()).done(function(mod1result, mod2result) {
doMod1Update(mod1result);
doMod2Update(mod2result);
});
This only gets ugly if you call your resolve functions with multiple arguments, as jQuery is a bit inconsistent there and does not really distinguish multiple arguments from one array argument.
To uncouple them with that publish-subscribe pattern you are using, I'd recommend the following:
function Combination() {
this.deferreds = [];
this.success = [];
this.error = [];
}
Combination.prototype.add = function(def, suc, err) {
this.deffereds.push(def);
this.success.push(suc);
this.error.push(err);
};
Combination.prototype.start = function() {
var that = this;
return $.when.apply($, this.deferreds).always(function() {
for (var i=0; i<that.deferreds.length; i++)
that.deferreds[i].done(that.success[i]).fail(that.error[i]);
// of course we could also call them directly with the arguments[i]
});
};
// Then do
var comb = new Combination();
window.notifyModules("something happened", comb); // get deferreds and handlers
comb.start();
// and in each module
window.listen("something happended", function(c) {
c.add(doRequest(), doUpdate, doErrorHandling);
});
Let's assume your modules look something like this :
var MODULE_1 = function() {
function getSnippet() {
return $.ajax({
//ajax options here
});
}
return {
getSnippet: getSnippet
}
}();
var MODULE_2 = function() {
function getSnippet() {
return $.ajax({
//ajax options here
});
}
return {
getSnippet: getSnippet
}
}();
Don't worry if your modules are different, the important thing is that the getSnippet functions each return a jqXHR object, which (as of jQuery 1.5) implements the Promise interface.
Now, let's assume you want to fetch the two snippets in response to some event (say a button click) and do something when both ajax responses have been received, then the click handler will be something like this:
$("myButton").on('click', function(){
var snippets = [];
var promises_1 = MODULE_1.getSnippet().done(function(response){
snippets.push({
target: $("#div_1"),
response: response
});
});
var promise_2 = MODULE_2.getSnippet().done(function(response){
snippets.push({
target: $("#div_2"),
response: response
});
});
$.when(promise_1, promise_2).done(function() {
$.each(snippets, function(i, snippetObj) {
snippetObj.target.html(snippetObj.response);
});
});
});
Slightly more elaborate, and better if you have many similarly constructed modules to fetch many snippets, would be something like this:
$(function(){
$("myButton").on('click', function(){
var promises = [];
var snippets = [];
var modules = [MODULE_1, MODULE_2, MODULE_3 .....];
for (var i=1; i<=10; i++) {
promises.push(modules[i].getSnippet().done(function(response){
snippets.push({
target: $("#div_" + i),
response: response
};
}));
}
$.when.apply(this, promises).done(function() {
$.each(snippets, function(i, snippetObj) {
snippetObj.target.html(snippetObj.response);
});
});
});
});
As you can see, I've made heaps of assumptions here, but you should get some idea of how to proceed.
To ensure each callback is passed the appropriate arguments I've done the following:
var guid = 0,
deferreds = [];
window.request = function (url, deferred, success) {
var requestId = guid++;
if ($.inArray(deferred) === -1) {
deferreds.push(deferred);
$.extend(deferred, {
requestCount: 0,
responseCount: 0,
args: {}
});
}
deferred.requestCount++;
deferred
.done(function () {
// Corresponding arguments are passed into success callback using requestId
// which is unique to each request.
success.apply(this, deferred.args[requestId]);
});
$.ajax(url, {
success: function () {
// Store arguments on deferrds args obj.
deferred.args[requestId] = arguments;
deferred.responseCount++;
if (deferred.requestCount === deferred.responseCount) {
deferred.resolveWith(this);
}
}
});
};
So the arguments are managed through the closure. This allows me to ensure that both modules have no knowledge of each other and won't break if the other doesn't exist, e.g:
var MODULE_1 = function () {
$(".myButton").on('click', function() {
// Cross module communication is achieved through notifications.
// Pass along a new deferred object with notification for use in window.request
window.notify('my-button-clicked', new $.Deferred);
});
}();
var MODULE_2 = function () {
// run get snippet when 'my-button-clicked' notification is fired
window.listen('my-button-clicked', getSnippet);
function getSnippet (deferred) {
window.request('/module2', deferred, function () {
console.log('module2 success');
});
}
}();
var MODULE_3 = function () {
// run get snippet when 'my-button-clicked' notification is fired
window.listen('my-button-clicked', getSnippet);
function getSnippet (deferred) {
window.request('/module3', deferred, function () {
console.log('module3 success');
});
}
}();
The above allows each module to function independently meaning one will work without the other which loosely couples the code and because both MODULE_2 and MODULE_3 pass the same deferred object into window.request they will be resolved once both requests have successfully returned.
This was my final implementation:
https://github.com/richardscarrott/ply/blob/master/src/ajax.js