Chaining promises with $q in Angular - angularjs

I am trying to chain promises so that doQuery(0) executes then doQuery(1), etc... sequentially until doQuery(9).
My problem is that i is always equals to 10 in the callback function.
doQuery(0) executes then doQuery(10).
How do I pass each value of i in the callback function?
var promise = doQuery(0);
for (var i = 1; i < 10; i++) {
promise = promise.then(function() {
doQuery(i);
});
};

Since you're using Angular.js, you should use it's bind function here:
var promise = doQuery(0);
for (var i = 1; i < 10; i++) {
promise = promise.then(angular.bind(null, doQuery, i));
}
Without relying on Angular.js, you could use a closure to make a copy of i for each callback function (rather than having them all share the single copy of i in the outer scope):
var promise = doQuery(0);
for (var i = 1; i < 10; i++) {
promise = promise.then(function(i){
return function(){
doQuery(i);
};
}(i));
}
In modern Javascript engines you can also use the native Function.prototype.bind:
var promise = doQuery(0);
for (var i = 1; i < 10; i++) {
promise = promise.then(doQuery.bind(null, i));
}

You need to return each to-be-chained promise from the then callback, otherwise it will likely fail. To pass the right i value to each callback, see JavaScript closure inside loops – simple practical example.
var promise = doQuery(0);
for (var i=0; i<10; i++) (function(ii) {
promise = promise.then(function() {
return doQuery(ii);
});
})(i);

Related

AngularJS - create object array where label is $translated

I have the following array:
vm.roles = ['ROLE1', 'ROLE2', 'ROLE3', 'ROLE4'];
and I need this form of array:
vm.translatedRoles = [{id:0, label:'Role1'}, {id:1, label:'Role2'}, ...]
Therefore I wrote this function to transfer from vm.roles to vm.translatedRoles.
My Problem now is that translatedRoles stays empty, so there are no objects in it. Does anyone know why?
function translateRoles() {
var translatedRoles = [];
for(var i = 0; i < vm.roles.length; i++) {
$translate(vm.roles[i]).then(function(text) {
var role = {};
role.id = i;
role.label = text;
translatedRoles.push(role);
});
}
return translatedRoles;
}
That can't work. $translate() returns a promise. The function passed to $translate is executed later, asynchronously, when the translations are available. So, the return statement happens before translatedRoles is populated by the function.
You need to return a promise of array, or hope that the translations are already available and use $translate.instant():
function translateRoles() {
var translatedRoles = [];
for (var i = 0; i < vm.roles.length; i++) {
translatedRoles.push({
id: i,
label: $translate.instant(vm.roles[i]);
});
}
return translatedRoles;
}

How to Wait for Multiple ngResource $resource Queries to Resolve

I have these ngResource queries in my controller:
Ages.query(function(ages){
$scope.ages = ages;
});
Skinissues.query(function(skinissues){
$scope.skinissues = skinissues;
});
Skintypes.query(function(skintypes){
$scope.skintypes = skintypes;
});
Activities.query(function(activities){
$scope.activities = activities;
});
In the same controller in findOne function:
$scope.findOne = function() {
Products.get({
productId: $routeParams.productId
}, function(product) {
for (var i = 0; i < product.ages.length; i ++){
for (var j = 0; j < $scope.ages.length; j ++){
if (product.ages[i].id == $scope.ages[j].id){
$scope.ages[j]['ticked'] = true;
}
}
}
for (var i = 0; i < product.activities.length; i ++){
for (var j = 0; j < $scope.activities.length; j ++){
if (product.activities[i].id == $scope.activities[j].id){
$scope.activities[i]['ticked'] = true;
}
}
}
for (var i = 0; i < product.skintypes.length; i ++){
for (var j = 0; j < $scope.skintypes.length; j ++){
if (product.skintypes[i].id == $scope.skintypes[j].id){
$scope.skintypes[i]['ticked'] = true;
}
}
}
for (var i = 0; i < product.skinissues.length; i ++){
for (var j = 0; j < $scope.skinissues.length; j ++){
if (product.skinissues[i].id == $scope.skinissues[j].id){
$scope.skinissues[i]['ticked'] = true;
}
}
}
for (var i = 0; i < $scope.parents.length; i ++){
if ($scope.parents[i].id == product.parent.id){
$scope.parents[i]['ticked'] = true;
}
}
console.log('Products', product);
$scope.product = product;
});
};
This code, sometimes, it works, sometimes it doesn't, because in the findOne function, sometimes, the $scope.ages $scope.skinissues $scope.skintypes or $scope.activities is undefined.
This happens because their queries haven't finished yet.
What can I do to solve this problem?
Please help. Thanks.
Use $q.all to resolve the $resource promises.
angular.module('mean.variables')
.factory('Variables', function($q, Products, Ages, Activities, Skinissues, Skintypes, _){
return {
get: function(){
var promiseHash = {};
promiseHash.ages = Ages.query().$promise;
promiseHash.skinissues = Skinissues.query().$promise;
promiseHash.skintypes = Skintypes.query().$promise;
promiseHash.activities = Activities.query().$promise;
promiseHash.parents = Products.query().$promise;
return $q.all(promiseHash);
}
}
});
The above example function returns a promise that either resolves sucessfully to a hash of the query objects or resolves rejected with the first rejected response object.
The advantage of using $q.all() instead of $q.defer is that the promise chains aren't broken and error responses are retained for clients of the factory.
From the Docs:
The Resource instances and collections have these additional properties:
$promise: the promise of the original server interaction that created this instance or collection.
On success, the promise is resolved with the same resource instance or collection object, updated with data from server. This makes it easy to use in resolve section of $routeProvider.when() to defer view rendering until the resource(s) are loaded.
On failure, the promise is rejected with the http response object, without the resource property.
If an interceptor object was provided, the promise will instead be resolved with the value returned by the interceptor.
--AngularJS ngResource API Reference
all(promises);
Combines multiple promises into a single promise that is resolved when all of the input promises are resolved.
Parameters
An array or hash of promises.
Returns
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.
--AngularJS $q Service API Reference -- $q.all
Thanks guy, I've came up with this service by using setInterval to check if all variable are defined.
angular.module('mean.variables')
.factory('Variables', ['$q', 'Products', 'Ages', 'Activities', 'Skinissues', 'Skintypes', '_', function($q, Products, Ages, Activities, Skinissues, Skintypes, _){
return {
get: function(){
var variables = {};
var defer = $q.defer();
Ages.query(function(res){
variables.ages = res;
});
Skinissues.query(function(res){
variables.skinissues = res;
});
Skintypes.query(function(res){
variables.skintypes = res;
});
Activities.query(function(res){
variables.activities = res;
});
Products.query(function(res){
variables.parents = res;
});
var timer = setInterval(function(){
if (variables.ages && variables.activities && variables.skinissues && variables.skintypes && variables.parents){
defer.resolve(variables);
clearInterval(timer);
}
}, 50);
return defer.promise;
}
}
}])

In Angular, what's the best way to persist data through an asynchronous call?

I have a controller with a for loop that make's HEAD requests for an array of URLs to check if the file exists. When I get the response from the HEAD request, i need the index number from the array that the request was based on.
var allFiles = [], files = [];
allFiles.push({"url":"http://www.example.com/foo","source":"source1"});
allFiles.push({"url":"http://www.example.com/bar","source":"home"});
allFiles.push({"url":"http://www.example.com/wtf","source":"outer space"});
for(var i=0,len=allFiles.length;i<len;i++) {
$http.head(allFiles[i].url).then(function(response) {
files.push(allFiles[VALUE_OF_i_AT_TIME_OF_REQUEST]);
}
}
EDIT:
Because it is an asynchronous call, I cannot use i in place of VALUE_OF_i_AT_TIME_OF_REQUEST. Doing that results in i always being equal to len-1
I guess I can send the index number as data with the request and retrieve it from the response but for some reason that seems hack-ish to me.
Is there a better way?
You can do this with a function closure
for (var i = 0, len = allFiles.length; i < len; i++) {
function sendRequest(index) {
$http.head(allFiles[index].url).then(function (response) {
files.push(allFiles[index]);
});
}
sendRequest(i);
}
I may be oversimplifying this (asynchronous code is still tricky to me), but could you set i to a new local variable j on each loop then reference j instead of i in files.push(allFiles[j]):
var allFiles = [], files = [];
allFiles.push({"url":"http://www.example.com/foo","source":"source1"});
allFiles.push({"url":"http://www.example.com/bar","source":"home"});
allFiles.push({"url":"http://www.example.com/wtf","source":"outer space"});
for(var i = 0, len = allFiles.length; i < len; i++) {
var j = i;
$http.head(allFiles[i].url).then(function(response) {
files.push(allFiles[j]);
}
}
I did something similar to #rob's suggestion and it seems to be doing the trick.
var allFiles = [], files = [];
allFiles.push({"url":"http://www.example.com/foo","source":"source1"});
allFiles.push({"url":"http://www.example.com/bar","source":"home"});
allFiles.push({"url":"http://www.example.com/wtf","source":"outer space"});
for(var i=0,len=allFiles.length;i<len;i++) {
(function(i) {
$http.head(allFiles[i].url).then(function(response) {
files.push(allFiles[i]);
}
})(i);
}

How to do paginated API calls until reaching the end?

I imagine this could be a pretty general problem, but in this case I'm using AngularJS and the SoundCloud API.
Here's the flow:
Call loadTracks()
loadTracks() should load the tracks of a SoundCloud user, 50 at a time, until the list runs out.
loadTracks() does this by calling another function, sc.getTracksByUser(id), which returns a promise
loadTracks() should update the variable $scope.tracks with each 50 track batch when it arrives
The SoundCloud API provides an option offset, so loading the batches is relatively easy. I think it's the promise that is tripping me up. Without the promise, the solution would be:
$scope.tracks = [];
var loadTracks = function() {
var page = -1,
done = false,
newTracks;
while (!done) {
newTracks = getFiftyTracks(page++);
for (var i = 0; i < newTracks.length; i++) {
$scope.tracks.push(newTracks[i]);
}
if (newTracks.length < 50) done = true;
}
}
Unfortunately, that line with getFiftyTracks in it is not how it works. The actual implementation (using a promise) is:
sc.getTracksByUser(id).then(function (response) {
for (var i = 0; i < response.length; i++) {
$scope.tracks.push(response[i]);
}
}
I'm guessing the solution to this is some sort of recursion, but I'm not sure.
You can do that in this way
sc.getTracksByUser(id).then(function (response) {
for (var i = 0; i < response.length; i++) {
$scope.tracks.push(response[i]);
}
// if response return 50 track call getTracksByUser again
if (response.length === 50) sc.getTracksByUser(id);
});

synchronous for loop with promises

I have the following function that works but not as I want it to since each iteration needs the results of the iteration before.
There are plenty of similar questions but I'm finding it hard to reduce the solutions down to a pattern.
How can I rewrite the following function so that each loop iteration "waits" for the one before it?
$scope.updateBarcode = function() {
var itemcodes = $scope.itemcode.split(';');
// doc is JSPDF document, fetch will add a new page to the supplied doc.
var doc;
//fetch will unshift the returned doc onto the supplied array.
var result = [];
for (var i = 0; i < itemcodes.length; i++) {
var promise = $scope.fetch(doc, itemcodes[i],result);
promise.then(function(){
doc = result[0];
if (i >= itemcodes.length) {
doc.save(itemcodes[0] + '.pdf');
};
})
};
}
You need to assign the new promise from then() to your variable so that you build up a chain:
promise = promise.then(...);

Resources