AngularJS chaining promises - need to do work before the next 'then' - angularjs

I am working on a promise chain. The first call is an $http call to check if a user exists, and then if it does, theres a bunch of .then() statements that run sequentially.
My question is this.. in that first call, i don't want to return the promise of the $http request because if the user doesn't exist, the results are just an empty array and the promise resolves, thus triggering the next action to look up information about the user. I wrote the following code...
(see the part in comments about being the important part i'm asking about)
$scope.checkIfUserExists = function() {
if (angular.isObject($scope.admin.Inductee.Contactor)) {
var handleFault = function( fault ) {
if (typeof(fault) === 'string') {
switch (fault.toUpperCase()){
case 'NODATA':
// Go ahead an save
$scope.pushInductee();
break;
case 'STATUS':
// just get the 'duplicate records check' sign off of there
// The save button is disabled by the critical error
$scope.hideSave = false;
break;
case 'ASSIGNED':
// just get the 'duplicate records check' sign off of there
// The save button is disabled by the critical error
$scope.hideSave = true;
break;
default:
$log.error(fault);
$location.path('/error/default');
}
} else {
$log.error(fault);
$location.path('/error/default');
}
};
$scope.getMatchingIndData()
.then($scope.procBusLogic)
.then($scope.pushInductee)
.catch(handleFault);
}
};
////HERE IS THE IMPORTANT PART I AM ASKING ABOUT
$scope.getMatchingIndData = function() {
var deferred = $q.defer();
var locals = {};
var checkUser = function(dupeJson){
var checkUserDeferred = $q.defer();
// abandoned promise replaced with my own
sttiJoinDataFactory.checkIfUserExistsNurseleader(dupeJson)
.then(function(results) {
var data = results.data;
if (angular.isArray(data) && data.length > 0){
var highestMatch = data[0];
for (var i = 0; i < data.length; i++) {
if (parseInt(data[i].Score) > parseInt(highestMatch.Score)) {
highestMatch = data[i];
}
}
checkUserDeferred.resolve(highestMatch);
} else {
// Reject the 'overall' promise here
// to effectively break the chain
return deferred.reject('NODATA');
}
})
.catch(function(fault) {
// Any other failure should break the chain
// of http requests at this point
return deferred.reject(fault);
});
return checkUserDeferred.promise;
},
loadindividual = function (highestMatch) {
return $http stuff about the highestmatch
// set data in locals
},
parallelLoadStatusAndInducteeData = function(individual) {
return another $http promise based on the last then()
// set data in locals
},
loadCeremonyData = function (inductees){
return another $http promise based on the last call then() // set data in locals
},
reportProblems = function( fault ) {
deferred.reject(fault);
};
checkUser($scope.generateDupJson())
.then(loadindividual, reportProblems)
.then(parallelLoadStatusAndInducteeData, reportProblems)
.then(loadCeremonyData, reportProblems)
.then(function() {
deferred.resolve(locals);
})
.catch( reportProblems );
return deferred.promise;
};
Must I take into account the abandoned promise, since I really need to promise to resolve when the data comes back, and i need to reject it if there is NODATA. This is handled in the calling function's chain.
Also, I'm aware of antipatterns here. I'm trying my best to not nest promises, maintain the chain, as well as handle exceptions.

Ok I have a few comments for you:
...
// revert if and return immediately
// to reduce indentation
if (typeof(fault) !== 'string') {
$log.error(fault);
$location.path('/error/default');
return;
}
switch (fault.toUpperCase()) {
...
You don't need deferred objects:
var checkUser = function(dupeJson){
// this is not abandoned because we are returning it
return sttiJoinDataFactory.checkIfUserExistsNurseleader(dupeJson)
.then(function(results) {
var data = results.data;
if (!angular.isArray(data) || data.length <= 0) {
return $q.reject('NODATA');
}
var highestMatch = data.reduce(function (highest, d) {
return parseInt(d.Score) > parseInt(highest.Score) ?
d : highest;
}, data[0]);
return highestMatch;
}); // you don't need catch here if you're gonna reject it again
}
...
checkUser(...)
// loadIndividual will be called
// after everything inside checkUser resolves
// so you will have your highestMatch
.then(loadIndividual)
.then(parallelLoadStatusAndInducteeData)
.then(loadCeremonyData)
// you don't need to repeat reportProblems, just catch in the end
// if anything rejects prior to this point
// reportProblems will be called
.catch(reportProblems)
...

Related

Angular template won't load. Even with $loaded. Data resolves after Load

Using AngularFire, Angular, Firebase.
I load a list of users from a Firebase Database. I use $loaded to ensure it waits until data loads.
I take this list, compare it against another firebase database of groups and push the results into two arrays.
Based on the console.logs the data sorts correctly. However, inside my template I get a blank page (I think this is because the page loads before the data is sorted).
Thoughts?
let userLoggedIn = AuthFactory.getUser();
var allUsersArray = $firebaseArray(ConnectFactory.fbUserDb);
var x = firebase.database().ref('groups');
var friendArr = [];
var notFriendArr = [];
allUsersArray.$loaded().then(function(){
angular.forEach(allUsersArray, function(user, i) {
var haveIAdded = x.child(userLoggedIn).child(allUsersArray[i].uid).once('value').then(function (snap) {
if (snap.val() !== null) {
return true;
} else {
return false;
}
});
var haveTheyAdded = x.child(allUsersArray[i].uid).child(userLoggedIn).once('value').then(function (snap) {
if (snap.val() !== null) {
return true;
} else {
return false;
}
});
Promise.all([haveIAdded, haveTheyAdded]).then(function([you, they]) {
if (you && they) {
console.log('We Are Friends', allUsersArray[i]);
friendArr.push(allUsersArray[i]);
} else {
console.log('not a friend ', allUsersArray[i]);
notFriendArr.push(allUsersArray[i]);
}
});
});
$scope.friendList = friendArr;
$scope.notFriendList = notFriendArr;
});
Alright, this time I tried to actually read the question before attempting to answer. ;-)
When you set your $scope.friendList and $scope.notFriendList within the $loaded promise, your Promise.all may (and most likely) havn't resolved yet when those are called, since angular.forEach doesn't wait for the promises to finish before moving on to the next statement in the function. So you'll have to build an array of promises and wait for them all to resolve outside of the loop before attempting to set your $scope variables.
allUsersArray.$loaded().then(function(){
var promises = [];
var friendArr = [];
var notFriendArr = [];
angular.forEach(allUsersArray, function(user, i) {
... // Same as before
promises.push(
Promise.all([haveIAdded, haveTheyAdded]).then(function([you, they]) {
if (you && they) {
console.log('We Are Friends', allUsersArray[i]);
friendArr.push(allUsersArray[i]);
} else {
console.log('not a friend ', allUsersArray[i]);
notFriendArr.push(allUsersArray[i]);
}
})
);
});
Promise.all(promises).then(function(){
$scope.friendList = friendArr;
$scope.notFriendList = notFriendArr;
});
});

Prevent multiple submits in angularjs

I'm looking for a AngularJS-based way to prevent multiple submits per task.
I don't need buttons to be disabled after submission or close the form and wait for the task to be completed. Instead, I need requests to be unique.
To be more detailed, I need $http.get and $http.post stop sending multiple same requests.
Any Ideas?
According to this article, you can use provider decorator.
NOTE: this approach is based on angular-api
https://gist.github.com/adambuczynski/354364e2a58786e2be71
UPDATE
I've changed a little part in your suggested solution, because returned promises have lost .success and .error and .then.
Just use this edited code to have all of those functions working:
.config(["$provide", function ($provide) {
$provide.decorator('$http', function ($delegate, $q) {
var pendingRequests = {};
var $http = $delegate;
function hash(str) {
var h = 0;
var strlen = str.length;
if (strlen === 0) {
return h;
}
for (var i = 0, n; i < strlen; ++i) {
n = str.charCodeAt(i);
h = ((h << 5) - h) + n;
h = h & h;
}
return h >>> 0;
}
function getRequestIdentifier(config) {
var str = config.method + config.url;
if (config.data && typeof config.data === 'object') {
str += angular.toJson(config.data);
}
return hash(str);
}
var $duplicateRequestsFilter = function (config) {
if (config.ignoreDuplicateRequest) {
return $http(config);
}
var identifier = getRequestIdentifier(config);
if (pendingRequests[identifier]) {
if (config.rejectDuplicateRequest) {
return $q.reject({
data: '',
headers: {},
status: config.rejectDuplicateStatusCode || 400,
config: config
});
}
return pendingRequests[identifier];
}
pendingRequests[identifier] = $http(config);
$http(config).finally(function () {
delete pendingRequests[identifier];
});
return pendingRequests[identifier];
};
Object.keys($http).filter(function (key) {
return (typeof $http[key] === 'function');
}).forEach(function (key) {
$duplicateRequestsFilter[key] = $http[key];
});
return $duplicateRequestsFilter;
})
}])
It could be a performance issue but following idea could solve your problem.
Store the each request URL and DATA as key value pair on a variable. URL should be KEY. For Same URL multiple submission can be stored in a Array.
Then for any new call check the URL if it present in your stored object, then compare the data with each object thorughly (deep check, it is costly though).
If any exact match found then stop the processing. As same request came.
Other wise proceed and don't forget to store this data also.
But it is costly since need to check the data which could be havy.
Note: At the time of storing the data you could convert it to JSON String so it will be easier to compare between String.
here is the Code Algo
YourService.call(url, params) {
var Str1 = JSON.stringify(params);
if(StoredObj[url]) {
for each (StoredObj[url] as Str){
if(Str === Str1) {
return;
}
}
}
else {
StoredObj[url] = []; //new Array
}
StoredObj[url].push(Str1);
Call $http then;
}

Angularjs for loop issue

There's a for loop and inside the for loop I'm calling an AJAX request. The issue I encountered is, the for loop finishes before the requests complete.
I want the for loop to continue to it's next iteration only after the required AJAX request completes.
PS- AJAX works fine. I do get my desired information from the server. It's just the for loop iterations complete first without waiting for the AJAX request success function to fire up. So when the AJAX success function finally fires the value in the variable cid is inconclusive as it has been overwritten by the last iteration of the for loop.
I want the for loop to continue only after the AJAX success function is executed.
Code:
if (window.cordova) {
db = $cordovaSQLite.openDB("my.db"); //device
} else {
db = window.openDatabase("my.db", '1', 'my', 1024 * 1024 * 100); // browser
}
var query = "SELECT * FROM order_product";
$cordovaSQLite.execute(db, query, []).then(function(res) {
if (res.rows.length > 0) {
for (var i = 0; i < res.rows.length; i++) {
console.log(" foreach SELECTED shopcart-> " + res.rows.item(i).id);
var cid = res.rows.item(i).coffee_id;
$http.post("http://192.168.1.4/coffeepayWeb/public/index.php/getsugar", {
cid: cID
})
.success(function(result) {
console.log("success");
if (cid == 6) {
//do something
} else {
//do something else
}
});
}
}
}
Using for is unsafe for asynchronous operations if you need the iteration index, use it only to store the required values to make the async operation ($http.post in this case), it should looks like:
var items = [];
for (var i = 0; i < res.rows.length; i++) {
var item = res.rows.item(i);
items.push(item);
}
after consider that $http returns a promise, then you should be able to map all elements from items
var getsugarUrl = 'http://192.168.1.4/coffeepayWeb/public/index.php/getsugar';
// Map the values to obtain the promises on each $http operation, the allSettled method of the
// [Kriskowal Q library](https://github.com/kriskowal/q/wiki/API-Reference) will be simulated
// this is because when one of the http requests fail then the $q.all method break and reject with an error
var promises = items.map(function (item) {
var cid = item.coffee_id;
return $http.post(getsugarUrl, { cid: cid })
.then(function (result) {
// You can take advantage of this closure to handle the result of the request an the
// cid property, store your modified result on the value property from the return
return {
state: 'fullfilled',
value: {
result: result,
cid: cid
} // your modified result
};
})
// Handle when the http request fails
.catch(function (err) {
return {
state: 'rejected',
error: err
};
});
});
finally handle the results obtained using $q.all (you need to inject the $q service)
$q.all(promises)
.then(function (responses) {
// iterate over the results
responses
.filter(function(response) { // only fullfilled results
return response.state == 'fullfilled';
})
.forEach(function (response) {
if (response.value.cid == 6) {
//do something with response.value.result
} else {
//do something else
}
});
});
With this solution the http requests aren't resolved sequentially, but you have control over when they've finished together and you will have the correct value of cid
Check more about JavaScript Promises
$http uses promises, which means you need to think of the problem within the promise paradigm.
Consider a recursive option where you pass in an array of your cID's, and each call sends a $http.post for the 1st cID in the array; if the call succeeded, we continue recursively with a smaller array, until there are no more left.
One promise is created & returned in the 1st call, which is notified on each successful query (allowing you to do your per-cID logic), and finally resolved when all queries are done (or rejected if any query fails).
// This function is called without deferred;
// deferred is used on recursive calls inside the function
function doPost(url, cidList, deferred) {
if (deferred === undefined) {
deferred = $q.defer();
}
var cid = cidList[0];
$http.post(url, {cid: cid})
.success(function(result) {
// query succeeded; notify the promise
deferred.notify({cid: cid, result: result});
if (cidList.length > 1) {
// there are more items to process; make a recursive
// call with cidList[1:end]
doPost(url, cidList.slice(1), deferred);
} else {
// we're done; resolve the promise
deferred.resolve();
}
})
.error(function(message) {
// there was an error; reject the promise
deferred.reject({cid: cid, message: message});
});
return deferred.promise;
}
// build the list of cIDs to pass into doPost
var cidList = [];
for (var i = 0; i < res.rows.length; i++) {
cidList.push(res.rows.item(i).coffee_id);
}
// start the queries
doPost("http://192.168.1.4/coffeepayWeb/public/index.php/getsugar", cidList)
.then(function() {
// promise resolved
console.log("All done!");
}, function(info) {
// promise rejected
console.log("Failed on cID " + info.cid + ": " + info.message);
}, function(info) {
// promise being notified
console.log("Just did cID " + info.cid + ": " + info.result);
// your per-cid handler
if (info.cid == 6) {
// do something
} else {
// do something else
}
});
UPDATE
Since the motivation for the question had more to do with variable scope (rather than sequential HTTP requests), this is all you really need:
// Build the CID list so that we can iterate it
var cidList = [];
for (var i = 0; i < res.rows.length; i++) {
cidList.push(res.rows.item(i).coffee_id);
}
// Iterate the list & call $http.post
cidList.forEach(function(cid) {
// your $http.post() logic; each call will have its own
// cid thanks to closures
});
Each iteration will have it's own cid, which you can use in your .success() or .error() handlers without worrying about it being overwritten. As with the other solution, the requests aren't sequential, but you probably didn't need them to be in the first place.

How can I wait for $http response before continuing with angular.forEach loop

I'm making an AJAX request for each item in a loop, the end REST service can only perform one request at a time so I need the loop to wait for each request to complete before continuing with the next. How do I do this?
For reference, the end service is performing update tasks on DynamoDB tables - only one table can be modified at once hence my requirement to wait until I get a response before continuing. I could send them all to the server in one hit and handle there, although that makes it hard to receive feedback when each update is completed.
angular.forEach($scope.someArray,
function (value) {
var postdata = {
bla: value
};
$http.post('/some_url', postdata)
.then(
function(result) {
console.log("Success.");
},
function(data) {
console.log("Failure.");
}
);
}
);
Do you really need the forEach? I would not use it and go for something like that:
function req(arr) {
if (angular.isArray(arr) && arr.length > 0) {
var postdata = {
bla: arr[0]
};
$http.post('/some_url', postdata)
.then(
function(result) {
console.log("Success.");
arr.shift();
req(arr);
},
function(data) {
console.log("Failure.");
// if you want to continue even if it fails:
//arr.shift();
//req(arr);
}
);
}
}
req($scope.someArray);
If you really must make one request at a time (are you sure there isn't a more efficient alternative?), then you'll have to use something other than Angular.forEach.
What about something like this (you will need to inject $q):
function doWhateverWithAll(someArray) {
// Mark which request we're currently doing
var currentRequest = 0;
// Make this promise based.
var deferred = $q.deferred();
// Set up a result array
var results = []
function makeNextRequest() {
// Do whatever you need with the array item.
var postData = someArray[currentRequest].blah;
$http.post('some/url', postData)
.then( function (data){
// Save the result.
results.push(data);
// Increment progress.
currentRequest++;
// Continue if there are more items.
if (currentRequest < someArray.length){
makeNextRequest();
}
// Resolve the promise otherwise.
else {
deferred.resolve(results);
}
});
// TODO handle errors appropriately.
}
// return a promise for the completed requests
return deferred.promise;
}
Then, in your controller/service, you can do the following:
doWhateverWithAll($scope.someArray)
.then(function(results){
// deal with results.
});

Angular how to deal with unavailable URLs requested by $http.get or $http.jsonp, which are executed by $q.all()

I've the following code:
eventResourcesCall = $http.jsonp('https://apicall/to/serverA');
eventsDetailsCall = $http.get('https://apicall/to/serverB');
$q.all([eventResourcesCall, eventsDetailsCall]).then(function(values){
//process data manipulation and merging
});
The problem is that serverA and ServerB might not be available sometimes, and when one of those are unavailable, the data processing code stops and I get an error similar to the one described below:
GET https://apicall/to/serverA?jsonp=angular.callbacks._0 404 (Not Found)
Can any one point me to a documentation or describe on the answer how to properly deal with unavailable URL requested by $http and executed by $q.all()?
What I would like to be able to do is to get an indication that the URL is not accessible and then avoid the data processing code abortion.
Thanks!
I would use indirect promises:
var d1 = $q.defer(), d2 = $q.defer();
function NetworkError(reason) { this.reason = reason; }
eventResourcesCall = $http.jsonp('https://apicall/to/serverA').then(
function(response) {
d1.resolve(response);
},
function(err) {
d1.resolve(new NetworkError(err));
}
);
eventsDetailsCall = $http.get('https://apicall/to/serverB').then(
function(response) {
d2.resolve(response);
},
function(err) {
d2.resolve(new NetworkError(err));
}
);
$q.all([d1, d2]).then(function(values){
var eventResources = values[0], eventsDetails = values[1];
if( eventResources instanceof NetworkError ) {
// handle error
}
else {
// eventResources is good, use it
}
// and so on...
});
So the indirect promises are allways resolved and the all() succeeds. But the resolution value may be of the special NetworkError class which signals the actual error in this request.
This is definitely bulky, but could be improved with some utility methods, e.g.:
function makeIndirectPromise(httpPromise) {
var ret = $q.defer();
httpPromise.then(
function(response) {
ret.resolve(response);
},
function(err) {
ret.resolve(new NetworkError(err));
}
);
return ret.promise;
}
And the code above changes to:
function NetworkError(reason) { this.reason = reason; }
function makeIndirectPromise(httpPromise) { /* see above */ }
eventResourcesCall = makeIndirectPromise($http.jsonp('https://apicall/to/serverA'));
eventsDetailsCall = makeIndirectPromise($http.get('https://apicall/to/serverB'));
$q.all([eventResourcesCall, eventsDetailsCall]).then(function(values){
var eventResources = values[0], eventsDetails = values[1];
if( eventResources instanceof NetworkError ) {
// handle error
}
else {
// eventResources is good, use it
}
// and so on...
});
From Angular doc to $q: as $http returns a promise, you can catch promise rejection using either:
$q.all([eventResourcesCall, eventsDetailsCall]).then(function(values){
//process data manipulation and merging on Success
}).catch(function(errors){
//Deal with your $http errors
}).finally(function(data){
});
or
$q.all([eventResourcesCall, eventsDetailsCall]).then(function(values){
//process data manipulation and merging on Success
}, function(errors){
//Deal with your $http errors
});

Resources