I want to resolve a recursive data structure before calling a controller, but I'm struggling with how to do this.
my current code looks like this:
region.loadChildren = function() {
region.children = [];
return region.resource.all('children').getList().then(function(subs) {
angular.forEach(subs, function(sub) {
sub.loadChildren();
region.children.push(sub);
});
}, errorCallback
);
};
can I somehow "collect" the promises returned by sub.loadChildren() and combine them?
Yep, you can use $q.all to gather the promises into a single promise that resolves when each promise in the list has resolved.
var promises = []
angular.forEach(subs, function(sub) {
promises.push(sub.loadChildren());
region.children.push(sub);
});
$q.all(promises).then(function(values) {
// Finished
})
$q.all is youre solution -
region.loadChildren = function() {
region.children = [];
return region.resource.all('children').getList().then(function(subs) {
var promises = subs.map(subs, function(sub) {
var promise = sub.loadChildren();
region.children.push(sub);
return promise;
});
var allPromisesInOnePromise = $q.all(promises); // a regular promise which will resolve when all of the promises will resolve
}, errorCallback
);
};
you can read more about promises here and the $q.all function here- https://docs.angularjs.org/api/ng/service/$q
Use $q.all to consolidate promise collections and be sure to return them for chaining.
region.loadChildren = function() {
//return for chaining
return region.resource.all('children').getList().then(function(subs) {
var children = [];
angular.forEach(subs, function(sub) {
var p = sub.loadChildren();
children.push(p);
});
//return for chaining
return $q.all(children);
);
};
Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises. It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.1
For more information, see AngularJS $q service API Reference -- chaining promises.
Be aware that $q.all is not resilient. If one of the promises is rejected, $q.all will resolve rejected with the first error.
You can replace your current function with one that creates the initial array into which the promises will be accumulated, and then pass that to a modified version of your original function that does the real work:
region.loadChildren = function() {
var tmp = [];
this.loadChildrenHelper(tmp);
this.children = tmp;
}
region.loadChildrenHelper = function(accum) {
return region.resource.all('children').getList().then(function(subs) {
angular.forEach(subs, function(sub) {
sub.loadChildrenHelper(accum);
accum.push(sub);
});
}, errorCallback
);
};
and then use $q.all on the resulting array
Related
I have one Angular.js method from child controller, where it makes a call to twop parent controller methods one after another. But the second function should get the account data from from the first function and then will update call data like below:
filter.filterAccountsByProductMetrics1 = function(productWithSegmentations12) {
accountService.fetchAccountForRecordType([filter.selectedHcpHco.Name.display])
.then(function(resp) {
$scope.accountDataUpdate({
accounts: resp.data
});
var productId = null;
if(filter.selectedMySetupProduct.Product_vod__c) {
productId = filter.selectedMySetupProduct.Product_vod__c.value;
}
callService.getCallsForProductId(productId)
.then(function(calls) {
filter.filterRecords[filterType.product.value] = calls;
$scope.callDataUpdate({
calls: applyAllFilterOnCalls()
});
});
});
};
I've checked both the functions are getting called but the sequence is not maintained. How to make sure the two parent functions get called one after another.
EDIT: function accountDataUpdate:
call.accountDataUpdate = function(accounts) {
call.accounts = accounts;
getCallDetails(getCallIdsFromCallsForFilteredAccount())
.then(function() {
updateProductFrequencyTableData();
updateAccountDetailData(true);
});
updateDailyFrequencyChartData();
updateWeeklyFrequencyChartData();
updateCallFrequencyTableData();
updateAccountFrequencyData();
$timeout(function() {
$scope.$broadcast('updateDoughnutChart');
$scope.$broadcast('updateBarChart');
});
};
Modify accountDataUpdate to return a promise:
call.accountDataUpdate = function(accounts) {
call.accounts = accounts;
var promise = getCallDetails(getCallIdsFromCallsForFilteredAccount())
.then(function() {
updateProductFrequencyTableData();
updateAccountDetailData(true);
updateDailyFrequencyChartData();
updateWeeklyFrequencyChartData();
updateCallFrequencyTableData();
updateAccountFrequencyData();
return $timeout(function() {
$scope.$broadcast('updateDoughnutChart');
$scope.$broadcast('updateBarChart');
});
});
return promise;
};
Then use that promise for chaining:
filter.filterAccountsByProductMetrics1 = function(productWithSegmentations12) {
return accountService.fetchAccountForRecordType([filter.selectedHcpHco.Name.display])
.then(function(resp) {
return $scope.accountDataUpdate({
accounts: resp.data
});
}).then(function() {
var productId = null;
if(filter.selectedMySetupProduct.Product_vod__c) {
productId = filter.selectedMySetupProduct.Product_vod__c.value;
}
return callService.getCallsForProductId(productId)
}).then(function(calls) {
filter.filterRecords[filterType.product.value] = calls;
return $scope.callDataUpdate({
calls: applyAllFilterOnCalls()
});
});
};
Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises.
It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.
For more information, see AngularJS $q Service API Reference - Chaining promises.
I have the following q.all calling to resolve two promises. I checked all the posts and tried all other ways of implementation q.all and its the same case
var xyzdeffered = $q.defer();
service1.getServiceDetail1($routeParams.id).then(function(promise) {
xyzdeffered.resolve(promise);
});
var abcdeffered = $q.defer();
service2.getServiceDetail2($routeParams.id).then(function(promise) {
abcdeffered.resolve(promise);
});
$q.all([ xyzdeffered, abcdeffered ]).then(function(data) {
$scope.variable = data;
});
I am expecting the variable in q.all should get populated only after the first two promises are resolved. But unfortunately the service call itself doesn't get returned with data and the control moves on to the q.all. I find this weird because as per documentation the q.all is called only when your promises are returned with 200 response and are resolved.
I checked analysing the network calls and also put some alert to see the sequence of the code and find that the q.all alert is the first alert to be popped up and then the other promises are resolved.
Its really making me mad as why a simple implementation of q.all doesnt work..
Any help will be appreciated.
Why not directly call $q.all on first promise ?
$q.all([
service1.getServiceDetail1($routeParams.id),
service2.getServiceDetail2($routeParams.id)
]).then(function(data) {
//Array of result [resultOfgetServiceDetails1, resultOfgetServiceDetails2]
$scope.variable = data;
});
You need to reference the promise on the $q.defer() return object:
$q.all([ xyzdeffered.promise, abcdeffered.promise ])
For example you have you run multiple sq-lite queries synchronously just pass the array of queries(you can pass anything) as args into the call of this method.
function methodThatChainsPromises(args,tx) {
var deferred = $q.defer();
var chain = args.map(function(arg) {
var innerDeferred = $q.defer();
tx.executeSql(arg,[], function(){
console.log("Success Query");
innerDeferred.resolve(true);
}, function(){
console.log("Error Query");
innerDeferred.reject();
});
return innerDeferred.promise;
});
$q.all(chain).then(
function(results) {
deferred.resolve(true)
console.log("deffered resollve"+JSON.stringify(results));
}, function(errors) {
deferred.reject(errors);
console.log("deffered rejected");
});
return deferred.promise;
}
Im trying to iterate over an array that I construct from multiple http calls inside a angular.forEach()
the function
$scope.ticket_stats = function(){
//cleaning variables
$scope.data_set = [];
$scope.closed_tickets = [];
//fetching time stamps (epoch)
$scope.time_frame = time_period.days(7);
//calling data using time stamps
angular.forEach($scope.time_frame, function(item) {
//debug
console.log(item);
var promise = tickets.status("closed", item);
promise.success(function(data){
console.log(data);
$scope.closed_tickets.push(data[0].datapoints[0][0]); // returns a numerical value
});
});
//SEE MESSAGE BELOW
$scope.data_set.push($scope.closed_tickets);
}
the last line $scope.data_set.push() is working but increment itself over time once calls return data. I would like this line to be executed once everything within the for Each loop is all done. I need to iterate over the $scope.closed_tickets array afteward to play (addition) data inside it and build up a second array.
here are the services used in this function:
// CALL TICKETS STATS
app.service('tickets', function($http){
this.status = function(status, date){
var one_snap = date - 100;
var url = "/url/render?format=json&target=sum(stats.tickets."+status+")&from="+one_snap+"&until="+date+"";
return $http.get(url);
};
});
// TIME STAMPS MATHS
app.service('time_period', function(){
var currentDate = parseInt((new Date).getTime()/1000);
this.days = function(number){
var pending = [];
for (var i = number; i > 0; i--) {
pending.push(currentDate - (87677*i));
}
return pending;
};
});
I search for information and found out about the $q.all() service but didn't manage to make this work the way I want.
Any advices would be welcomed!
Thanks!
You can use $q.all to wait for multiple ansynchronous events (promises) to finish.
$scope.ticket_stats = function() {
// list of all promises
var promises = [];
//cleaning variables
$scope.data_set = [];
$scope.closed_tickets = [];
//fetching time stamps (epoch)
$scope.time_frame = time_period.days(7);
//calling data using time stamps
angular.forEach($scope.time_frame, function(item) {
// create a $q deferred promise
var deferred = $q.defer();
//debug
console.log(item);
tickets.status("closed", item).success(function(data) {
console.log(data);
$scope.closed_tickets.push(data[0].datapoints[0][0]);
// promise successfully resolved
deferred.resolve(data);
});
// add to the list of promises
promises.push(deferred.promise);
});
// execute all the promises and do something with the results
$q.all(promises).then(
// success
// results: an array of data objects from each deferred.resolve(data) call
function(results) {
$scope.data_set.push($scope.closed_tickets);
},
// error
function(response) {
}
);
}
First, deferred represents a piece of code that will take an unknown amount of time to execute (asynchronous). deferred.resolve(data) simply states that the code is finished. Data could be anything, an object, string, whatever, but it is usually the results of your asynchronous code. Likewise you can reject a promise with deferred.reject(data) (maybe an error was thrown by the sever). Again, data can be anything but here it should probably be the error response.
deferred.promise just returns a promise object. The promise object allows you to set callbacks like .then(successFunction, errorFunction) so you know a piece of code has finished executing before moving on to successFunction (or errorFunction in the case of a failure). In our case $q has the .all method which waits for an array of promises to finish then gives you the results of all the promises as an array.
Don't forget to inject the $q service.
Try to make an array of promises only, without resolving them yet. Then aggregate them with $q.all(). After aggregated promise resolve iterate through the array of those promises again. Now you are sure that they are all resolved.
var promises = [];
angular.forEach($scope.time_frame, function(item) {
promises.push(tickets.status("closed", item));
});
var aggregatedPromise = $q.all(promises);
aggregatedPromise.success(function(){
angular.forEach(promises, function(promise) {
promise.success(function(data){
$scope.closed_tickets.push(data[0].datapoints[0][0]); // returns a numerical value
});
});
});
Maybe this is not the most efficient way to do this, but I think that should solve your problem.
Even though you mention $q.all didn't work for you, as I don't see why it should't, here's how I would do this.
Basically you want to map an array of things (time stamps) to some other things (promises in this case since we have async calls) and do some action on the resulting array.
var promises = $scope.time_frame.map(function (item) {
return tickets.status("closed", item);
});
Now we use $q.all to wait for all promises to resolve:
$q.all(promises).then(function (tickets) {
$scope.data_set.push(tickets);
});
Ok, this is a re-posting of a previous question which I'd confused by over-simplifying the code...
I have an angularjs factory function which queries a parse database, and returns a promise ... so far, pretty simple... however, my controller has a foreach loop which calls the function for each of a range of ids, which are meant to then resolve concurrently. The resolve doesn't work, however, and what I get back appears to be a garbled mixture of the api calls.
Here's a simplified version of my code...
app.factory('MyFactory', function($http, $q ) {
return {
testFunction: function (ingredientList) {
var deferred = $q.defer()
ingredientIndex=-1
var regulariseIngredient=function() {
ingredientIndex++
if (ingredientIndex>ingredientList.length-1) {
deferred.resolve(ingredientList);
return
}
(new Parse.Query("Ingredient"))
.equalTo("type", ingredientList[ingredientIndex].type)
.equalTo("name", ingredientList[ingredientIndex].name)
.include("parent")
.find().then(function(result) {
result=result[0]
if(result.get("parent")) result=result.get("parent")
ingredientList[ingredientIndex]=result
regulariseIngredient();
})
}
regulariseIngredient()
return deferred.promise
}
}
}
app.controller('TestController', function($scope,MyFactory) {
recipes = [ An array of objects, each with an ingredients array]
angular.forEach(recipes, function(recipe) {
MyFactory.testFunction(recipe.ingredients).then(function(result) {
console.log(result)
})
})
}
Bit more info on what the service actually does...
my app has a collection of recipes, and a collection of ingredients. Each ingredient has a 'parent' which substitutes it in the 'rationalised' version. The test function loops through each ingredient, compiles the outcome and then returns a collection of substituted ingredients. The controller is looping through the recipes, so that I can check the ingredients on hand of all recipes.
#MarkPeace, it appears that you have hit on the anti-pattern "outlawed" here under the heading "The Collection Kerfuffle".
The espoused pattern (for performing an asynchronous task, in parallel, on each member of an array of items) is :
function workMyCollection(arr) {
return q.all(arr.map(function(item) {
return doSomethingAsync(item);
}));
}
In your case, doSomethingAsync() is (new Parse.Query("Ingredient"))...find();
In full, the code will be something like :
app.factory('MyFactory', function($http, $q) {
return {
testFunction: function (ingredientList) {
return $q.all(ingredientList.map(function(item) {
return (new Parse.Query("Ingredient"))
.equalTo("type", item.type)
.equalTo("name", item.name)
.include("parent")
.find().then(function(result) {
return result[0].get("parent") || result[0];//phew, that's a bit more concise
});
}));
}
}
});
testFunction() will thus return a promise of an array of results (whatever they happen to be).
In the controller, you can use the same pattern again to loop through the recipes :
app.controller('TestController', function($scope, MyFactory) {
var recipes = [ An array of objects, each with an ingredients array ];
$q.all(recipes.map(function(recipe) {
return MyFactory.testFunction(recipe.ingredients).then(function(results) {
console.dir(results);
return results;
});
})).then(function(arr) {
//all done-and-dusted
//If everything has been successful, arr is an array of arrays of results
//Do awesome stuff with the data.
})['catch'](function(err) {
//report/handle error here
});
});
Of course, exactly how much you do in the controller and how much in the factory (or factories) is up to you. Personally, I would choose to keep the controller somewhat simpler.
Using $q.all() which takes an array of promises as argument and returns a new promise which is:
resolved if all promises of the array are resolved
rejected if any of the promise of the array is rejected
from the doc:
Returns a single promise that will be resolved with an array/hash of values, each value corresponding to the promise at the same index/key in the promises array/hash. If any of the promises is resolved with a rejection, this resulting promise will be rejected with the same rejection value.
For the loop, using Array.forEach is cleaner:
testFunction: function (ingredientList) {
var promiseArray = [];
ingredientList.forEach(function (ingredient) {
var promise = (new Parse.Query("Ingredient"))
.equalTo("type", ingredient.type)
.equalTo("name", ingredient.name)
.include("parent")
.find().then(function(result) {
result = result[0]
if(result.get("parent")) {
result = result.get("parent");
}
return result;
});
promiseArray.push(promise);
});
return $q.all(promiseArray);
}
I have a little question about multiple promise.
How can I wait that all promises will be done for return the final result.
See my code :
getInfo : function(){
return promiseA.then(function(result){
info = result;
//this function have also promises
return ServiceA.functionA(info.login)
.then(function(favouriteItems){
info.favorites = favouriteItems;
return $q.when(info);
});
});
},
My aims it's to wait the result of ServiceA.functionA before return value.
Thanks
K.L
I wrote an answer to another question stating the solution to this problem using the $q.all approach.
Check it out: AngularJS: Listen to events, one after the other
You need to use $q.all()
This is a good post here on the question: stackoverflow.com/questions/21310964/angularjs-q-all
function getInfo() {
var deferred = $q.defer();
promiseA.then(function(result){
info = result;
// this function have also promises
ServiceA.functionA(info.login)
.then(function(favouriteItems){
info.favorites = favouriteItems;
// the magic
deferred.resolve(info);
// at this point, you can resolve any value
});
});
}
return deferred.promise;
}
then later you can call that function and get a promise...
var promise = getInfo();
promise.then(successCallback, errorCallback, notifyCallback);
The successCallback will only be called once resolve has been called on the deferred object.
getInfo : function() {
return promiseA.then(function(result){
return ServiceA.functionA(result.login).then(function(favouriteItems){
result.favorites = favouriteItems;
return result;
});
});
},
Use like this:
api.getInfo().then(function(result){
// use result and result.favorites
}, function(err){
// here you can be if an error occurred in promiseA or in ServiceA.functionA
})