Chaining Angular Promises - angularjs

I am having some trouble chaining promises in Angular. What I want to do is fetch my project object from the API, then check if the project owner has any containers, if they do, trigger the another GET to retrieve the container. In the end the container assigned to scope should either be null or the object retrieved from the API.
Right now, this example below resolves immediately to the second then function, and I get the error, TypeError: Cannot read property 'owner' of undefined. What am I doing wrong?
$http.get('/api/projects/' + id).then(function (data) {
$scope.project = data.project;
return data.project;
}).then(function (project) {
var containers = project.owner.containers;
if (containers.length) {
return $http.get('/api/containers/' + containers[0]);
} else {
return null
}
}).then(function (container) {
$scope.container = container;
});

Ah, turns out the data from passed into then is inside a field, so I needed to do
$scope.project = data.data.project;
return data.data.project;

Your example code works, but what if the $http call fails because of a 404? Or you want to later want to add some extra business logic?
In general you want to handle 'negative' cases using a rejecting promise, to have more control over the chaining flow.
$http.get('/api/projects/' + id).then(function (data) {
$scope.project = data.data.project;
return data.data.project;
}).then(function (project) {
var containers = project.owner.containers;
if (containers.length) {
return $q.reject('containers empty');
}
return $http.get('/api/containers/' + containers[0]);
}).then(function (container) {
$scope.container = container;
}).except(function (response) {
console.log(response); // 'containers empty' or $http response object
$scope.container = null;
});

Related

Http service delay processing until after data is returned

I work on my angularjs project.
I created this service:
(function () {
"use strict";
angular.module("manageItems").factory("manageItemsService", ["$http", "config", manageItemsService]);
function manageItemsService($http, config) {
var service = {
getNewItems: getNewItems,
};
return service;
function getNewItems(session, mapName) {
return $http.get(serviceUrl + 'getNewItems/' + session + "/" + mapName);
}
}
})();
And here how I call the service from controller:
function getNewItems() {
manageItemsService.getNewItems(mapguideService.mapName, mapguideService.sessionId).then(function (result) {
self.currentItems = result.data;
})
}
I need to make service to delay while the response returned.
How can I change servicefunction to make it wait until self.currentItems property is populated by data?
First I need say that http requests are actually performed asynchronously so as not to stop the application while the result is returned.
So you have two options, use the angular pattern to adjust your method in order to treat the result, so you must pass a callback function to the service, so that the service, not the controller make the association. It would be something like:
Service:
(function () {
"use strict";
angular.module("manageItems").factory("manageItemsService", ["$http", "config", manageItemsService]);
function manageItemsService($http, config) {
var service = {
getNewItems: getNewItems,
};
return service;
function getNewItems(session, mapName, callback, errorCallback) {
$http.get(serviceUrl + 'getNewItems/' + session + "/" + mapName).then(callback, errorCallback);;
}
}
})();
Controller:
function getNewItems() {
manageItemsService.getNewItems(mapguideService.mapName, mapguideService.sessionId, function (result) {
//this callback will be called asynchronously when the response is available
self.currentItems = result.data;
}, function(error) {
// called asynchronously if an error occurs
// or server returns response with an error status.
})
}
The second option is to totally not recommended, inserting a loop while the result is expected... (to bad)
I hope I have helped!
Then you could be put up .then on getNewItems $http call. And based on retrieved response data, decide whether to return data or call another service method.
function anotherFunction(){
return $http.get(url);
}
function getNewItems(session, mapName) {
return $http.get(serviceUrl + 'getNewItems/' + session + "/" + mapName).then(function successCallback(response){
var data = response.data;
//call another function if data is empty
if(!data.length)
return anotherFunction(); //make sure another function should return promise
return data;
});
}
What the code needs to do is chain promises.
To make the getNewItems function chainable, return the derived promise:
function getNewItems() {
//vvvv RETURN promise
return manageItemsService.getNewItems(mapguideService.mapName, mapguideService.sessionId)
.then(function (response) {
self.currentItems = response.data;
//RETURN value to chain
return response.data;
});
};
Then use the returned promise to chain more operations:
getNewItems().then( function(currentItems) {
//Evaluate current Items
if ( ok ) {
return "DONE";
} else {
//RETURN to chain something else
return getOtherItems();
};
}).then( function(otherItems) {
if (otherItems == "DONE") return;
//ELSE
self.otherItems = otherItems;
//Do further chaining
});
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.
— AngularJS $q Service API Reference - Chaining Promises

Promise Chains in angularjs

I am uploading attachments using rest api in SharePoint 2013,for this I need to call upload attachment method on synchronous.
Because If I call upload attachment method asynchronous I am getting 409 conflict error.
How to chain promise objects in for loop.i.e I want to call second attachment method in first attachment success and so on..
Please help me in best approach of chaining of promises in for loop.
Common method for saving attachments:
var saveFileAngularJS = function (file, url) {
var deferred = $q.defer();
getFileBuffer(file).then(function (fileArrBuffer) {
$http({
method: 'POST',
url: baseUrl + url,
headers: {
'Accept': 'application/json;odata=verbose',
'Content-Type': undefined,
'X-RequestDigest': jQuery("#__REQUESTDIGEST").val()
},
data: new Uint8Array(fileArrBuffer),
transformRequest: []
}).then(function successCallback(data) {
deferred.resolve(data);
alert('Successfully saved.', data);
}, function errorCallback(error) {
deferred.reject(error);
alert('Failed to save!!!.', error);
});
});
return deferred.promise;
};
Method calling :
for (var i = 0; i < $scope.files.length; i++) {
var file = $scope.files[i]._file;
var response = lssDealService.insertAttachment(transactionId, file);
}
var insertAttachment = function (dealId, file) {
var attachmentUrl = listEndPoint + "/GetByTitle('TransactionList')/GetItemById(" + dealId + ")/AttachmentFiles/add(FileName='" + file.name + "')";
return baseService.saveFile(file, attachmentUrl);
};
Insert attachment will call SaveFile method.
I want to run this for loop sequentially, once the loop has been completed I need to process all promises and display success message to user.
Please help me to writing the chaining promises in effective way.
Lets say you have the attachements as an array,
function uploadMyAttachements() {
return myAttachements.reduce(function(promise, attachment) {
return promise.then(function () {
return upload(attachment);
})
.then(function(result) {
console.log('RESULT FOR LAST UPLOAD', result);
});
}, Promise.resolve());
}
function upload(attachment) {
//upload the attachment to sharepoint
//and return a promise here
}
uploadMyAttachements().catch(function(err) {
//if anything in the promise chain fails
//it stops then and there and CATCHED here
});
Now whats happening here, using the Array.reduce, we create a chain of promises like shown below
upload(0).then(handleResult_0).upload(1).then(handleResult_1)....
and it execute one by one as you expected
Throwing my 2 pennies:
$scope.attachments = []; //modified via binding.
function uploadAttachments(){
//Reduce the files array into a promise array with the uploadOne method
//then return the promise when every promise has been resolved or one has rejected.
return $q.all($scope.attachments.reduce(uploadOne, []));
}
function uploadOne(file){
//Upload one, return promise. Use $http or $resource.
}
//Note - a more advanced way of doing this would be to send the files as batch (one
//$http post) as FormData. There are some good wrappers for angular.
$scope.upload = function(){
uploadAttachments().then(function(results){
//Array of results
}).catch(function(e){
//Error handler
});
}

How to call Success or Error on binded methods (Controllers, Factorys)

I am trying to figure out how handle success and error on binded methods that return an http response.
I am trying to bind a factory method to a controller, so I am able to call it from the view. This method will try to add an item to the shopping cart via an http request. If it fails the method that is being called will return false, however if it succeeds it will return the http response.
I want to be able to somehow add success or error callbacks to the binded method. Is this possible?
Car controller
// Controller to display cars page
app.controller('carsController', function($scope, $rootScope, inventoryFactory) {
// Function that will fetch JSON and save all necessary data for us to use
function init() {
// Bind these method calls to our cart factory
// Allow method to be called from the view
$scope.addToCart = userFactory.addToCart;
// Get list of items in car category
inventoryFactory.getItems('cars').success( function(data) {
$scope.items = data;
});
}
init();
});
userFactory
// Add item of given ID to shopping cart
factory.addToCart = function(itemID) {
// Validate our user / token
data = factory.getUserToken();
if (data === false) {
return false;
}
req = {
method: 'PUT',
url: 'routes.php/api/shoppingcart/' + itemID,
headers: {
'X-Api-Token': data.apiToken,
'UserID': data.userID
}
};
return $http(req);
};
What you are doing will work, but I would recommend a few changes.
First, direct binding may cause problems. If your factory method at any point needs to call this, it will lose it, since this becomes the $scope rather than the returned object from the factory.
$scope.addToCart = userFactory.addToCart; // addToCart will have "this" be of $scope
Whereas you can keep it either by proxying or by wrapping:
$scope.addToCart = function(id) {
return userFactory.addToCart(id);
};
guarantees that addToCart inside the factory has the correct context for this.
Second, while you can do what you want, if you are returning a promise (as $http() does), then sometimes returning false and sometimes returning a promise can lead to messy code and difficulty testing. You may be better off always returning a promise and rejecting it as necessary:
var defer, ret;
data = factory.getUserToken();
if (data === false) {
defer = $q.defer();
defer.reject();
ret = defer.promise;
} else {
req = {
method: 'PUT',
url: 'routes.php/api/shoppingcart/' + itemID,
headers: {
'X-Api-Token': data.apiToken,
'UserID': data.userID
}
};
ret = $http(req);
}
return ret;
This way you will always have a promise, and can always do
addToCart(25).then(function(){/* success */),function(){/* failure */});
If you need to do error handling, you might want to handle it inside your controller. So if your template is:
<button ng-click="addToCart(item.id)">Click me!</button>
Then your controller addToClick might be:
$scope.addToCart = function(id) {
userFactory.addToCart(id).then(function(results){
// indicate success on the screen by changing some scope var, e.g.
$scope.message = "Successfully added to cart";
},function(err){
// indicate error on the screen by changing some scope var, e.g.
$scope.message = "Problem adding to cart: "+err;
});
};

How to use restangular with promise pattern in angular service

I have a service with rest angular with following structure
function countrySvc(restangular) {
restangular.addResponseInterceptor(function (data, operation, what, url, response, deferred) {
if (operation === 'getList') {
var newResponse = response.data;
return newResponse;
}
return response;
});
var baseCountry = restangular.all('country');
this.countries = function() {
baseCountry.getList();
};
}
also a controller
function countryCtrl(scope, countrySvc) {
scope.countries = countrySvc.countries();
}
but when i access the countries from controller, the result is empty with a successful request with data, my question is how a can extract the data from response with proper promise pattern, ie( i need array of countries when i access scope.countries)
You need to resolve promise...
There are two ways to do it...
1) Using $object
just add .$object to end of promise so once request is done it resolves promise...
scope.countries = countrySvc.countries().$object;
2) Using then
if you need to do some stuff after promise is resolved pick this option, once request is done callback function in then will be fired
scope.countries = countrySvc.countries().then(function (response){
// DO SOMETHING IF YOU NEED BEFORE SET OBJECT
scope.countries = response;
// DO SOMETHING IF YOU NEED AFTER SET OBJECT
});

Cancelling a request with a $http interceptor?

I'm trying to figure out if it is possible to use a $http interceptor to cancel a request before it even happens.
There is a button that triggers a request but if the user double-clicks it I do not want the same request to get triggered twice.
Now, I realize that there's several ways to solve this, and we do already have a working solution where we wrap $http in a service that keeps track of requests that are currently pending and simply ignores new requests with the same method, url and data.
Basically this is the behaviour I am trying to do with an interceptor:
factory('httpService', ['$http', function($http) {
var pendingCalls = {};
var createKey = function(url, data, method) {
return method + url + JSON.stringify(data);
};
var send = function(url, data, method) {
var key = createKey(url, data, method);
if (pendingCalls[key]) {
return pendingCalls[key];
}
var promise = $http({
method: method,
url: url,
data: data
});
pendingCalls[key] = promise;
promise.finally(function() {
delete pendingCalls[key];
});
return promise;
};
return {
post: function(url, data) {
return send(url, data, 'POST');
}
}
}])
When I look at the API for $http interceptors it does not seem to be a way to achieve this. I have access to the config object but that's about it.
Am I attempting to step outside the boundaries of what interceptors can be used for here or is there a way to do it?
according to $http documentation, you can return your own config from request interceptor.
try something like this:
config(function($httpProvider) {
var cache = {};
$httpProvider.interceptors.push(function() {
return {
response : function(config) {
var key = createKey(config);
var cached = cache[key];
return cached ? cached : cached[key];
}
}
});
}
Very old question, but I'll give a shot to handle this situation.
If I understood correctly, you are trying to:
1 - Start a request and register something to refer back to it;
2 - If another request takes place, to the same endpoint, you want to retrieve that first reference and drop the request in it.
This might be handled by a request timeout in the $http config object. On the interceptor, you can verify it there's one registered on the current request, if not, you can setup one, keep a reference to it and handle if afterwards:
function DropoutInterceptor($injector) {
var $q = $q || $injector.get('$q');
var dropouts = {};
return {
'request': function(config) {
// I'm using the request's URL here to make
// this reference, but this can be bad for
// some situations.
if (dropouts.hasOwnProperty(config.url)) {
// Drop the request
dropouts[config.url].resolve();
}
dropouts[config.url] = $q.defer();
// If the request already have one timeout
// defined, keep it, othwerwise, set up ours.
config.timeout = config.timeout || dropouts[config.url];
return config;
},
'requestError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
},
'response': function(response) {
delete dropouts[response.config.url];
return response;
},
'responseError': function(reason) {
delete dropouts[reason.config.url];
return $q.reject(reason);
}
};
}

Resources