AngularJS: $q wait for all even when 1 rejected - angularjs

I've been trying to wait for a couple of promises with Angular's $q but there seems to be no option to 'wait for all even when a promis is rejected'.
I've created an example (http://jsfiddle.net/Zenuka/pHEf9/21/) and I want a function to be executed when all promises are resolved/rejected, is that possible?
Something like:
$q.whenAllComplete(promises, function() {....})
EDIT: In the example you see that the second service fails and immediately after that the function in $q.all().then(..., function(){...}) is being executed. I want to wait for the fifth promise to be completed.

Ok, I've implemeted a basic version myself (I only want to wait for an array of promises). Anyone can extend this or create a cleaner version if they want to :-)
Check the jsfiddle to see it in action: http://jsfiddle.net/Zenuka/pHEf9/
angular.module('test').config(['$provide', function ($provide) {
$provide.decorator('$q', ['$delegate', function ($delegate) {
var $q = $delegate;
// Extention for q
$q.allSettled = $q.allSettled || function (promises) {
var deferred = $q.defer();
if (angular.isArray(promises)) {
var states = [];
var results = [];
var didAPromiseFail = false;
if (promises.length === 0) {
deferred.resolve(results);
return deferred.promise;
}
// First create an array for all promises with their state
angular.forEach(promises, function (promise, key) {
states[key] = false;
});
// Helper to check if all states are finished
var checkStates = function (states, results, deferred, failed) {
var allFinished = true;
angular.forEach(states, function (state, key) {
if (!state) {
allFinished = false;
}
});
if (allFinished) {
if (failed) {
deferred.reject(results);
} else {
deferred.resolve(results);
}
}
}
// Loop through the promises
// a second loop to be sure that checkStates is called when all states are set to false first
angular.forEach(promises, function (promise, key) {
$q.when(promise).then(function (result) {
states[key] = true;
results[key] = result;
checkStates(states, results, deferred, didAPromiseFail);
}, function (reason) {
states[key] = true;
results[key] = reason;
didAPromiseFail = true;
checkStates(states, results, deferred, didAPromiseFail);
});
});
} else {
throw 'allSettled can only handle an array of promises (for now)';
}
return deferred.promise;
};
return $q;
}]);
}]);

Analogous to how all() returns an array/hash of the resolved values, the allSettled() function from Kris Kowal's Q returns a collection of objects that look either like:
{ state: 'fulfilled', value: <resolved value> }
or:
{ state: 'rejected', reason: <rejection error> }
As this behavior is rather handy, I've ported the function to Angular.js's $q:
angular.module('your-module').config(['$provide', function ($provide) {
$provide.decorator('$q', ['$delegate', function ($delegate) {
var $q = $delegate;
$q.allSettled = $q.allSettled || function allSettled(promises) {
// Implementation of allSettled function from Kris Kowal's Q:
// https://github.com/kriskowal/q/wiki/API-Reference#promiseallsettled
var wrapped = angular.isArray(promises) ? [] : {};
angular.forEach(promises, function(promise, key) {
if (!wrapped.hasOwnProperty(key)) {
wrapped[key] = wrap(promise);
}
});
return $q.all(wrapped);
function wrap(promise) {
return $q.when(promise)
.then(function (value) {
return { state: 'fulfilled', value: value };
}, function (reason) {
return { state: 'rejected', reason: reason };
});
}
};
return $q;
}]);
}]);
Credit goes to:
Zenuka for the decorator code
Benjamin Gruenbaum for pointing me in the right direction
The all implementation from Angular.js source

The promise API in angularJS is based on https://github.com/kriskowal/q. I looked at API that Q provides and it had a method allSettled, but this method has not been exposed over the port that AngularJS uses. This is form the documentation
The all function returns a promise for an array of values. When this
promise is fulfilled, the array contains the fulfillment values of the
original promises, in the same order as those promises. If one of the
given promises is rejected, the returned promise is immediately
rejected, not waiting for the rest of the batch. If you want to wait
for all of the promises to either be fulfilled or rejected, you can
use allSettled.

I solved this same issue recently. This was the problem:
I had an array of promises to handle, promises
I wanted to get all the results, resolve or reject
I wanted the promises to run in parallel
This was how I solved the problem:
promises = promises.map(
promise => promise.catch(() => null)
);
$q.all(promises, results => {
// code to handle results
});
It's not a general fix, but it is simple and and easy to follow. Of course if any of your promises could resolve to null then you can't distinguish between that a rejection, but it works in many cases and you can always modify the catch function to work with the particular problem you're solving.

Thanks for the inspiration Zenuka, you can find my version at https://gist.github.com/JGarrido/8100714
Here it is, in it's current state:
.config( function($provide) {
$provide.decorator("$q", ["$delegate", function($delegate) {
var $q = $delegate;
$q.allComplete = function(promises) {
if(!angular.isArray(promises)) {
throw Error("$q.allComplete only accepts an array.");
}
var deferred = $q.defer();
var passed = 0;
var failed = 0;
var responses = [];
angular.forEach(promises, function(promise, index) {
promise
.then( function(result) {
console.info('done', result);
passed++;
responses.push(result);
})
.catch( function(result) {
console.error('err', result);
failed++;
responses.push(result);
})
.finally( function() {
if((passed + failed) == promises.length) {
console.log("COMPLETE: " + "passed = " + passed + ", failed = " + failed);
if(failed > 0) {
deferred.reject(responses);
} else {
deferred.resolve(responses);
}
}
})
;
});
return deferred.promise;
};
return $q;
}]);
})

A simpler approach to solving this problem.
$provide.decorator('$q', ['$delegate', function ($delegate) {
var $q = $delegate;
$q.allSettled = $q.allSettled || function (promises) {
var toSettle = [];
if (angular.isArray(promises)) {
angular.forEach(promises, function (promise, key) {
var dfd = $q.defer();
promise.then(dfd.resolve, dfd.resolve);
toSettle.push(dfd.promise);
});
}
return $q.all(toSettle);
};
return $q;
}]);

A simple solution would be to use catch() to handle any errors and stop rejections from propagating. You could do this by either not returning a value from catch() or by resolving using the error response and then handling errors in all(). This way $q.all() will always be executed. I've updated the fiddle with a very simple example: http://jsfiddle.net/pHEf9/125/
...
function handleError(response) {
console.log('Handle error');
}
// Create 5 promises
var promises = [];
var names = [];
for (var i = 1; i <= 5; i++) {
var willSucceed = true;
if (i == 2) willSucceed = false;
promises.push(
createPromise('Promise' + i, i, willSucceed).catch(handleError));
}
...
Be aware that if you don't return a value from within catch(), the array of resolved promises passed to all() will contain undefined for those errored elements.

just use finally
$q.all(tasks).finally(function() {
// do stuff
});

Related

Angular & Jasmine: how to test that a $q promise chain was resolved

I have a Service that expose a function that receives a parsed CSV (using papaparse) and promise that reflects the parsing status:
If the file was missing mandatory fields, the promise is rejected
Otherwise, It parses each row into an item and auto populates the missing fields (the auto population process is asynchronous).
when all items are populated, the function resolves the promise with the items array
The function I want to test is onCsvParse:
angular.module('csvParser', [])
.factory('csvParser', ['$http',
function($http) {
var service = {
onCsvParse: function(results, creatingBulkItems) {
var errors = this.getCsvErrors(results);
if (errors.length > 0) {
//reject
creatingBulkItems.reject(errors);
} else {
var items = this.parseCsv(results);
var autoPopulateItems = [],
populatedItems = [];
for (var i = 0; i < populatedItems.length; i++) {
var item = items[i];
if (item.name === "" /*or some any field is missing */ ) {
// auto populate item
autoPopulateItems.push(this.autoPopulateItem(item));
} else {
var populatedItem = $q.when(item);
populatedItems.push(populatedItem);
}
}
populatedItems =autoPopulateItems.concat(populatedItems);
var populatingAllItems = $q.all(populatedItems);
populatingAllItems.then(function(items) {
creatingBulkItems.resolve(items);
}, function(err) {
creatingBulkItems.resolve(err);
});
}
},
autoPopulateItem: function(newItem) {
var populatingItem = $q.defer();
var item = angular.copy(newItem);
$http.post('api/getItemData', { /*.....*/ })
.success(function(response) {
//----Populate item fields
item.name = response.name;
//....
//resolve the promise
populatingItem.resolve(item)
}).error(err) {
// resolving on error for $q.all indication
populatingItem.resolve(item)
};
return populatingItem.promise;
}
}
return service;
}
])
My test for this method looks as follows (simplified):
describe('bulk items upload test', function() {
//upload csv & test scenarios...
var $rootScope, $q, csvResults = {};
var $httpBackend, requestHandler;
beforeEach(module('csvParser'));
beforeEach(inject(function(_$rootScope_, _$q_) {
$rootScope = _$rootScope_;
$q = _$q_;
}));
beforeEach(inject(function($injector) {
// Set up the mock http service responses
$httpBackend = $injector.get('$httpBackend');
// backend definition common for all tests
requestHandler = $httpBackend.when('POST', 'api/getItemData')
.respond({
name: "name",
description: "description",
imageUrl: "www.google.com"
});
// afterEach(function(){ $rootScope.$apply();});
}));
it('Should parse csv string', function(done) {
var csvString = "Name,Description of the page";//...
Papa.parse(csvString, {
complete: function(results) {
csvResults = results;
done();
}
});
});
it('Should fail', function(done) {
var creatingBulkItems = $q.defer();
console.log("here..");
csvParser.onCsvParse(csvResults, creatingBulkItems);
creatingBulkItems.promise.then(function() {
console.log("1here..");
//promise is never resolved
expect(1).toEqual(1);
done();
}, function() {
//promise is never rejeceted
console.log("2here..");
expect(1).toEqual(1);
done();
});
$rootScope.$apply();
});
});
With this I get the error: Error: Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
the promises are not resolved, although I called $rootScope.$apply() and I am also not calling a real asynchronous call (only mocks,except $q.all).
How can I make it work?
Invalid syntax. You need to pass a function to the error callback.
}).error(function(err) {
// resolving on error for $q.all indication
populatingItem.resolve(item)
});
return populatingItem.promise;
Also you jasime test require some more initialization:
http://plnkr.co/edit/wjykvpwtRA0kBBh3LcX3?p=preview
After reading this article: https://gist.github.com/domenic/3889970 I found out my problem.
The key point is to flatten the promise chains using the promise.then return value.
This [promise.then] function should return a new promise that is fulfilled when the given fulfilledHandler or errorHandler callback is finished. This allows promise operations to be chained together. The value returned from the callback handler is the fulfillment value for the returned promise. If the callback throws an error, the returned promise will be moved to failed state.
Instead of resolving the outer promise within the inner promise success/fail callbacks, the outer promise is resolved in the inner promise.then callbacks
So my fix is something like this:
onCsvParse: function(results) {
var errors = this.getCsvErrors(results);
if (errors.length > 0) {
var deferred = $q.defer();
//reject
return deferred.reject(errors);
} else {
var items = this.parseCsv(results);
var autoPopulateItems = [],
populatedItems = [];
for (var i = 0; i < populatedItems.length; i++) {
var item = items[i];
if (item.name === "" /*or some any field is missing */ ) {
// auto populate item
autoPopulateItems.push(this.autoPopulateItem(item));
} else {
var populatedItem = $q.when(item);
populatedItems.push(populatedItem);
}
}
populatedItems = autoPopulateItems.concat(populatedItems);
var populatingAllItems = $q.all(populatedItems);
return populatingAllItems.then(function(items) {
return items;
}, function(err) {
return err;
});
}
},
test code:
it('Should not fail :)', function(done) {
csvParser.onCsvParse(csvResults).then(function(items) {
//promise is resolved
expect(items).not.toBeNull();
done();
}, function() {
//promise is rejeceted
//expect(1).toEqual(1);
done();
});
$rootScope.$apply();});

$q.all promise is never makes to then

One of my promises during feeds.forEach cycle ends up with error. Maybe because this in LoadData never executes line $rootScope.links = urls; which is inside then. How to fix it?
app.service('LoadData', ['FeedService', 'EntryStateUrlService', '$q',
function(FeedService, EntryStateUrlService, $q) {
this.loadData = function(feedSrc) {
FeedService.parseFeed(feedSrc).then(EntryStateUrlService.getEntryStateUrl).then(function(data) {
console.log(data);
$rootScope.links = data;
});
}
}
]);
app.service('FeedService', function($http, $q) {
this.parseFeed = function(url) {
var deferred = $q.defer();
$http.jsonp('//ajax.googleapis.com/ajax/services/feed/load?v=1.0&callback=JSON_CALLBACK&q=' + encodeURIComponent(url))
.success(function(res) {
deferred.resolve(res.data.responseData.feed.entries);
}).error(function() {
deferred.reject();
});
return deferred.promise;
}
});
app.service('EntryStateUrlService', ['$state', '$q',
function($state, $q) {
this.getEntryStateUrl = function(feeds) {
var deferred = $q.defer();
var idx = 0,
promises = [],
promise = null;
feeds.forEach(function(e) {
promise = $http.jsonp(e.link).success(function(data) {
/*stuff*/
e['entryStateUrl'] = data.link; // UPDATED
deferred.resolve(data);
});
promises.push(promise);
}); //forEach
return $q.all(promises);
}
}
]);
UPDATE
I don't really understand how $q.all as a big promise object composite of many other promises will deliver data to service LoadData....
UPDATE2
It seems like since one promise fails (because the one of the urls is invalid) $q.all fails and never makes to then(). How to go around that? I need to get all data from all successful promises.
One thing is you'll need to inject $rootScope in order to use it:
app.service('LoadData', ['FeedService', 'EntryStateUrlService', '$q', '$rootScope',
function(FeedService, EntryStateUrlService, $q, $rootScope) {
//...
}
]);
Also in the Q library there's a method called allSettled that will allow you to view all the promise results even if one fails. The angular $q does not have this method, but several people have found it useful so they've created extensions for it. Here's an example of one on a Github Gist:
angular.module('qAllSettled', []).config(function($provide) {
$provide.decorator('$q', function($delegate) {
var $q = $delegate;
$q.allSettled = function(promises) {
return $q.all(promises.map(function(promise) {
return promise.then(function(value) {
return { state: 'fulfilled', value: value };
}, function(reason) {
return { state: 'rejected', reason: reason };
});
}));
};
return $q;
});
});
Using this method will return all results, whether rejected or not. The returned object will have a state property that will tell you the status of the promise:
return $q.allSettled(promises);
Then you can check the status of each promise and add it to the links as needed:
FeedService.parseFeed(feedSrc)
.then(EntryStateUrlService.getEntryStateUrl)
.then(function(results) {
$scope.links = [];
results.forEach(function (result) {
if (result.state === "fulfilled") {
$scope.links.push(result.value);
} else {
var reason = result.reason;
}
});
});
Plunker Demonstration

AngularJs $q.all no data

I really need your help with the following.
I inquire Firebase, get a list of tasks by priority (task date), then cycle the results.
For each task is read it's related job (another branch in Firebase), put the result on a task property (jobObject) then return a promise.
In the end, $q.all should return all results. But it just doesn't work.
What I'm doing wrong here?
http://jsfiddle.net/danielchindea/R4M7x/1/
var startAt = '2014-03-01',
endAt = '2014-03-31',
promises = [];
var getTask = function (task) {
var d = $q.defer();
jobRef.child(task.jobId).on('value', function (jobSnapshoot) {
task.jobObject = jobSnapshoot.val();
d.resolve(task);
});
return d.promise;
};
taskRef.startAt(startAt).endAt(endAt).on('value', function (tasksSnapshoot) {
angular.forEach(_.values(tasksSnapshoot.val()), (function (task) {
if (task) {
promises.push(getTask(task));
}
}));
console.log('finish');
});
// it was $q.all($scope.promises) but this isn't the issue
$q.all(promises).then(function (result) {
$scope.events = result;
console.log('results' + result);
});
The problem with your code is that this block is async:
taskRef.startAt(startAt).endAt(endAt).on('value', function (tasksSnapshoot) {
and When the further code gets executed, promises is still an empty array:
$q.all(promises).then(function (result) {
since promises is just an empty array (without unresolved promises) $q.all gets triggered immediately after the end of run stack (with an empty array as a result).
Solution - http://jsfiddle.net/R4M7x/7/
taskRef.startAt(startAt).endAt(endAt).on('value', function (tasksSnapshoot) {
angular.forEach(_.values(tasksSnapshoot.val()), (function (task) {
if (task) {
promises.push(getTask(task));
}
}));
$q.all(promises).then(function (result) {
$scope.events = result;
console.log('results' + result);
});
});
Your problem is that your running $q.all on $scope.promises and not promises

Angular: Rewriting function to use promise

I'm using an Angular factory that retrieves data from a feed and does some data manipulation on it.
I'd like to block my app from rendering the first view until this data preparation is done. My understanding is that I need to use promises for this, and then in a controller use .then to call functions that can be run as soon as the promise resolves.
From looking at examples I'm finding it very difficult to implement a promise in my factory. Specifically I'm not sure where to put the defers and resolves. Could anyone weigh in on what would be the best way to implement one?
Here is my working factory without promise:
angular.module('MyApp.DataHandler', []) // So Modular, much name
.factory('DataHandler', function ($rootScope, $state, StorageHandler) {
var obj = {
InitData : function() {
StorageHandler.defaultConfig = {clientName:'test_feed'};
StorageHandler.prepData = function(data) {
var i = 0;
var maps = StorageHandler.dataMap;
i = data.line_up.length;
while(i--) {
// Do loads of string manipulations here
}
return data;
}
// Check for localdata
if(typeof StorageHandler.handle('localdata.favorites') == 'undefined') {
StorageHandler.handle('localdata.favorites',[]);
}
},
};
return obj;
});
Here's what I tried from looking at examples:
angular.module('MyApp.DataHandler', []) // So Modular, much name
.factory('DataHandler', function ($rootScope, $q, $state, StorageHandler) {
var obj = {
InitData : function() {
var d = $q.defer(); // Set defer
StorageHandler.defaultConfig = {clientName:'test_feed'};
StorageHandler.prepData = function(data) {
var i = 0;
var maps = StorageHandler.dataMap;
i = data.line_up.length;
while(i--) {
// Do loads of string manipulations here
}
return data;
}
// Check for localdata
if(typeof StorageHandler.handle('localdata.favorites') == 'undefined') {
StorageHandler.handle('localdata.favorites',[]);
}
return d.promise; // Return promise
},
};
return obj;
});
But nothing is shown in console when I use this in my controller:
DataHandler.InitData()
.then(function () {
// Successful
console.log('success');
},
function () {
// failure
console.log('failure');
})
.then(function () {
// Like a Finally Clause
console.log('done');
});
Any thoughts?
Like Florian mentioned. Your asynchronous call is not obvious in the code you've shown.
Here is the gist of what you want:
angular.module("myApp",[]).factory("myFactory",function($http,$q){
return {
//$http.get returns a promise.
//which is latched onto and chained in the controller
initData: function(){
return $http.get("myurl").then(function(response){
var data = response.data;
//Do All your things...
return data;
},function(err){
//do stuff with the error..
return $q.reject(err);
//OR throw err;
//as mentioned below returning a new rejected promise is a slight anti-pattern,
//However, a practical use case could be that it would suppress logging,
//and allow specific throw/logging control where the service is implemented (controller)
});
}
}
}).controller("myCtrl",function(myFactory,$scope){
myFactory.initData().then(function(data){
$scope.myData = data;
},function(err){
//error loudly
$scope.error = err.message
})['finally'](function(){
//done.
});
});

How to use Backbone.Model.save() promise with validation

I'm trying to use the promise returned from Backbone.model.save(). Actually, per spec, it returns a promise if valid and false if not. I'd like to use the return value, regardless of the type, in future deferred.done() and deferred.fail() calls. Like this:
var promise = model.save();
$.when(promise).done(function() {
console.log('success!');
});
$.when(promise).fail(function() {
console.log('dang');
});
But, $.when() when passed a non-promise fires, done(), so, in the above, if the model is invalid, $.when(false).done() fires, and you get "success!".
I know I can use the success and error attributes in save(), but with my code, it's advantageous to have multiple done() functions applied later. That's the power of the promise afterall.
So, I'm left with:
var promise = model.save();
if (promise) {
$.when(promise).done(function() {
console.log('success!');
});
$.when(promise).fail(function() {
console.log('dang');
});
} else {
console.log('dang');
}
I hate not being DRY.
var promise = model.save();
var fail = function() {
console.log('dang');
};
if (promise) {
$.when(promise).done(function() {
console.log('success!');
});
$.when(promise).fail(function() {
fail();
});
} else {
fail();
}
Geting pretty messy. You get the picture. I'm hoping I'm just missing something here.
You can override Backbone.save method to have your desired behavior. If the returned value from the original save function is a boolean (which means validation failed), just return a custom promise and reject its related deferred.
var oldSaveFunction = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function(){
var returnedValue = oldSaveFunction.apply(this, arguments),
deferred = new $.Deferred();
if(_.isBoolean(returnedValue)){
deferred.reject();
return deferred.promise();
}
return returnedValue;
}
var Person = Backbone.Model.extend({
url : 'http://www.google.com',
validate : function(attributes){
if(!("name" in attributes))
return "invalid";
}
});
var person = new Person();
$.when(person.save()).fail(function(){
console.log("failed");
});
Try it on this fiddle
http://jsfiddle.net/WNHXz/1/
Here's an improvement on a previous answer, but with better support for error handling. I needed it to avoid swallowing errors so here's what I did:
var oldSaveFunction = Backbone.Model.prototype.save;
Backbone.Model.prototype.save = function () {
var returnedValue = oldSaveFunction.apply(this, arguments),
fulfiller,
rejecter,
pendingPromise = new Promise(function (fulfill, reject) {
fulfiller = fulfill;
rejecter = reject;
});
if (_.isBoolean(returnedValue)) {
rejecter(this.validationError);
} else {
// Assuming returnedValue is a deferred
returnedValue.then(function success() {
fulfiller.apply(this, arguments);
}, function failure(jqxhr, errorName, error) {
rejecter(error);
});
}
return pendingPromise;
};
Hope it helps!

Resources