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);
});
Related
I have been doing lots of searching trying to find a solution, and believe it ultimately comes down the the promise as my data is returned but all at the end, where as I need it through each iteration.
I have the vm.planWeek.dinner that I loop through each row, and append 'menuType' and the 'trackMenuIds' array to it, which I then use in my MongoDB call for search criteria. This all works fine, but the key element, is with each factory call returned, I add the returned item's id to the 'trackMenuIds' array. The reason for this is, it builds an array of items I already have returned, so they can ignored in the next call, ie via $nin.
vm.reviewWeek = function () {
//Array to be updated over each iteration and used in factory call
var trackMenuIds = [];
angular.forEach(vm.planWeek.dinner, function (day) {
//Adds two more items to day(current row) to pass to factory
day.menuType = 'dinner';
day.weekIds = trackMenuIds;
//Factory call - each time this runs, the 'trackMenuIds' array should have
//one more item added from the previous run
menuFactory.matchMenuCriteria(day)
.then(function (response) {
var randomItem = response.data[0];
day.menuItem = {'_id': randomItem._id, 'name': randomItem.name};
//adds the current id to the array to be used for the next run
trackMenuIds.push(randomItem._id);
});
});
};
When I append the 'trackMenuIds' array to the current row, it hasn't been updated with any id's. When I console it, I can see it does infact add them, but believe as its part of a promise, it is not doing it early enough to pass the updated array into my factory call over each iteration.
I have tried chain promises and other means but just can't seem to get it to work. Quite possibly it comes down to my inexperience of promises, so any help would be greatly appreciated.
The calls to the factory asynchronous API are being made in parallel. They need to chained sequentially:
vm.reviewWeek = function () {
//Array to be updated over each iteration and used in factory call
var trackMenuIds = [];
//INITIAL empty promise
var promise = $q.when();
angular.forEach(vm.planWeek.dinner, function (day) {
//Adds two more items to day(current row) to pass to factory
day.menuType = 'dinner';
day.weekIds = trackMenuIds;
//Factory call - each time this runs, the 'trackMenuIds' array should have
//one more item added from the previous run
//CHAIN sequentially
promise = promise.then(function () {
//RETURN API promise to chain
return menuFactory.matchMenuCriteria(day);
}).then(function (response) {
var randomItem = response.data[0];
day.menuItem = {'_id': randomItem._id, 'name': randomItem.name};
//adds the current id to the array to be used for the next run
trackMenuIds.push(randomItem._id);
});
});
return promise;
};
The above example creates an initial empty promise. The foreach loop then chains a call to the asynchronous API on each iteration.
You can use $q.all to handle multiple asynchronous call. Once all promise done execute, loop through raw Http promise and then push the data to the new array
vm.reviewWeek = function () {
//Array to be updated over each iteration and used in factory call
var trackMenuIds = [];
var dinnersPromise = [];
vm.planWeek.dinner.forEach(function (day, ind) {
//Adds two more items to day(current row) to pass to factory
day.menuType = 'dinner';
day.weekIds = trackMenuIds;
dinnersPromise.push(menuFactory.matchMenuCriteria(day));
});
$q.all(dinnersPromise).then(function (arr) {
angular.forEach(arr, function (response) {
var randomItem = response.data[0];
day.menuItem = {'_id': randomItem._id, 'name': randomItem.name};
//adds the current id to the array to be used for the next run
trackMenuIds.push(randomItem._id);
});
});
}
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
What am i trying to achieve is as such:
Invoking my service to retrieve all appointments in appointment types (number of types not fixed) tied to a doctor
If there are 3 appointment types, then there will be 3 async calls made
return a single promise with $q.all() after all 3 promises have been resolved
appointmentService
this.getAllDoctorAppointments = function (doctor, appointmentTypeArray) {
var promises = [];
angular.forEach(appointmentTypeArray, function (appointmentType) {
var defer = $q.defer();
$http.get('/appointments/?doctorName=' + doctor + '&apptType=' + appointmentType)
.success(function (listOfAppointments) {
defer.resolve(listOfAppointments);
promises.push(defer.promise);
});
});
return $q.all(promises);
};
In my console log, the appointmentType returns [ ].
This happens because the empty 'promises' array is returned even before the 3 async calls are made. I am still very new to the concept of promises, what is the best approach to work this out? Thanks!
$scope.getAllDoctorAppointments = function (doctor, appointmentTypeArray) {
appointmentService.getAllDoctorAppointments(doctor, appointmentTypeArray)
.then(function (appointmentType) {
//could take in any number. hardcoded 3 just for testing.
console.log(appointmentType)
angular.forEach(appointmentType[0], function (xRay) {
$scope.xRayAppts.events.push(xRay);
});
angular.forEach(appointmentType[1], function (ctScan) {
$scope.ctScanAppts.events.push(ctScan);
});
angular.forEach(appointmentType[2], function (mri) {
$scope.mriAppts.events.push(mri);
});
});
};
this.getAllDoctorAppointments = function (doctor, appointmentTypeArray) {
var promises = [];
angular.forEach(appointmentTypeArray, function (appointmentType) {
promises.push($http.get('/appointments/?doctorName=' + doctor + '&apptType=' + appointmentType)
.success(function (listOfAppointments) {
return listOfAppointments;
});
);
});
return $q.all(promises);
};
$http.get returns the promises that you wants to collect, there is no need for a new defer in this case.
the promise is not being added to the array because the code that adds it to the array, promises.push(defer.promise);, is inside of the result code of the thing you are trying to defer. So the promise wouldn't get added to the list of promises to execute until after it executed!
so you can either move that push line outside of the success call looking something like this:
angular.forEach(appointmentTypeArray, function (appointmentType) {
var defer = $q.defer();
$http.get('/appointments/?doctorName=' + doctor + '&apptType=' + appointmentType)
.success(function (listOfAppointments) {
defer.resolve(listOfAppointments);
});
promises.push(defer.promise);
});
or, you can do as lcycool suggests and just add the $http.get(...).success(...) calls to the array directly.
So been struggling a bit with getting promises to work properly, but after a lot of work, think I have gotten it. So now comes the question, can you create a self fulfilling promise, if you don't want to wait for the real thing...
Short pseudo version of what I want to do
var promise;
if (!factory.isDataLoaded()){
//The data is not loaded
var promise = factory.init();
} else {
//Data is all loaded
var promise = getSelfFullfilingPromise
}
//Some other code
promise.then(function({
//Do some stuff with the data from factory. which we know is loaded
})
Consider this option (taken straight from my app). I have a factory that loads up a couple of tables, especially one, it translates ids from one table to arrays of names, status and so forth from another table. Then the code does all kinds of wonderful things with it.
OK, it does some massaging and makes pretty diagrams. Problem is that if the initiation of the factory (i.e. do an API call, get some data, store it in a variable) haven't finished, half my page does not render, my boss gets angry, I get fired, and I'll have to dumpster dive behind McDonald (OK, not quite that bad).
So moved my init api call to a promise, sweet.
Then created a helper function that returns status (it checks if there is data in a variable and returns true or false). And I have the request it self (idGetSkill).
I then also have a directive which is called about 20 times, so I don't really want to call my init 20 times in order to ensure that there is data. I only whant to call it if the data is empty, or of skillLoaded returns false.
But if I use .then as a callback, I need a promise that resolves for it to run. So was thinking.
An example of where it could be used:
The Factory
.factory('skillFactory', function($http) {
var skillFactory = [];
var skills = [];
var searchId = [];
var mySkillId = [];
skillFactory.init= function() {
console.log("Got called")
return $http.get('/api/skillList')
.then(function(data){
skills=data['data']
console.log('Skill test ' + skills[0].alias );
})
}
skillFactory.skillLoaded=function(){
if(skills.length < 1) {
console.log("Warning, no data");
return false;
} else {
return true;
}
}
skillFactory.idGetSkill = function(data) {
if (skills.length < 1){
console.log("Warning, no data");
} else {
for (var id in skills) {
if (data == skills[id]._id) {
return skills[id];
}
}
}
}
}
And an app calling it
.controller("PromiseLoad", function ($scope, $http, $window, skillFactory ) {
var promise;
if( ! skillFactory.skillLoaded() ){
promise = skillFactory.init();
} else {
promise = skillFactory.init();
}
var skill = '55c8a069cca746f65c9836a1'
console.log("Will ask for skill " + skill)
promise.then( function() {
console.log("Im done waiting!")
$scope.answer = skillFactory.idGetSkill(skill);
console.log ("And got " +$scope.answer.alias);
})
});
(OK, the above example does not really need it, but its easier to provide this example rather than a directive as that needs a lot more things to work. Like data and stuff:) )
$q.when(data) returns a resolved promise.
$q.reject(data) returns a rejected promise.
I'm having some trouble managing a for loop with multiple promises. I've read that I should use $q.all, but I havent had any success with it thus far. I need this function to make sure all of the async calls inside the for loop have occurred.
refreshFeed: function(){
var defer = $q.defer();
var promises = []; // array of promises
for (var x = 0; x < userdata.length; x++){
this.getLatest(x).then(function(){
promises.push(x);
});
}
console.log(userdata); // this is updated successfully by getLatest(x)
$q.all(promises).then(function(){
defer.resolve();
})
return defer;
}
It successfully prints the console.log but then I get the console error "undefined is not a function" at the line that initially calls refreshFeed().
Any help would be appreciated. Thank you!
Edit:
getLatest: function(x){
var deferred = $q.defer();
var promise = twitterService.getLatestTweets(userdata[x].searchterm, userdata[x].sources).then(function(data) {
userdata[x].tweets = data;
console.log(userdata[x].tweets);
deferred.resolve();
});
return deferred.promise;
}
Update: I used PSL's answer and toyed around with getLatest and I finally got it to work. I just needed to replace the userdata[x]'s with the function parameter. Thanks #PSL
You would need to pass an array of promises and even if you are using deferred pattern you are not resolving anything. Assuming getLatest returns a promise you could just do:-
refreshFeed: function(){
return $q.all(userdata.map(this.getLatest));
}
Array.map