Angular $q promises and non-linear chaining - javascript

Hello everyone out there. I've hit a brick wall. I'm trying to research Angular async method calls, promise objects and implement a solution to a particular issue. After a few hours of trial and error and restructuring my code, I'm certain at this point the answer is right under my nose but I cannot see it.
I have a scenario where I must use $http to make a backend service call, which yields a promise. In the results of promise I will receive a data object with one string property and an array of 100 IDs. This backend service will only deliver payloads of 100 IDs at a time, so I must make x number of $http calls to reach the end of the list, as provided by this backend service. As I understand promises, I must evaluate the $http response in the promise's .then() method and if the string property is 'N' then I know I must call the backend service again for another batch of 100 IDs. After all IDs have been delivered the string will contain a 'Y' indicating that the end of this has been sent and don't call again. No comment required on this scheme, I know it's fairly lame, and unfortunately out of my control. ;-)
Everything I've studied regarding promises just seem to illustrate chaining of promises, in a linear fashion, if more async calls are needed. Whether the promise chain is nested or flat, it seems like I can only make a "known" or fixed number of sequential calls, e.g. Promise1 to Promise2, to Promise3, etc. Forgive my limited experience with promises here.
Here is a code example, you can see where I'm stuck here, nesting downward. I can't just "hope" after 3 or 5 or 10 calls I'll get all the IDs. I need to dynamically evaluate the result of each call and call again.
Here is the Angular service which invokes $http
(function() {
'use strict';
angular
.module('myapp')
.factory('IDservice', IDservice);
function IDservice($http) {
var service = {
model: {
error: {
state: false,
message: ''
},
ids: [],
end: '',
},
GetIDs: function() {
var request = 'some params...';
var url = 'the url...';
return $http.post(url, request).then(reqComplete).catch(reqFailed);
function reqComplete(response) {
service.model.ids = response.data.idList;
service.model.end = response.data.end;
return service.model;
}
function getIntradayMsrFailed(error) {
service.model.error.state = true;
service.model.error.message = error;
return service.model;
}
}
};
return service;
}
})();
Here is the Controller which invokes the Angular service, which ultimately drives some UI elements on the respective view:
(function() {
'use strict';
angular
.module('myapp')
.controller('AvailableIDsController', AvailableIDsController);
function AvailableIDsController($scope, $location, $timeout, IDservice) {
var vm = this;
vm.completeIDList = [];
activate();
function activate() {
IDservice.GetIDs().then(function(response){
vm.completeIDList = response.ids;
console.log('promise1 end');
if(response.end === 'N'){
IDservice.GetIDs().then(function(response){
angular.forEach(response.ids,function(nextID){
vm.comleteIDList.push(nextID);
});
console.log('promise2 end');
if(response.end === 'N'){
IDservice.GetIDs().then(function(response){
angular.forEach(response.ids,function(nextID){
vm.comleteIDList.push(nextID);
});
console.log('promise3 end');
});
}
});
}
});
console.log('mainthread end');
}
}
})();
You can see where that's going... and it's very, very ugly.
I need a way, inside the activate() method, to call a method which will take care of invoking the service call and return the result back up to activate(). Now, still in the activate() method, evaluate the result and determine whether to call again, etc. Where I'm stuck is once the main processing thread is done, you're left with program control in that first promise. From there, you can perform another promise, etc. and down the rabbit hole you go. Once I'm in this trap, all is lost. Clearly I'm not doing this right. I'm missing some other simple piece of the puzzle. Any suggestions would be so greatly appreciated!

You're looking for plain old recursion:
function AvailableIDsController($scope, $location, $timeout, IDservice) {
var vm = this;
vm.completeIDList = [];
return activate(0);
function activate(i) {
return IDservice.GetIDs().then(function(response) {
[].push.apply(vm.completeIDList, response.ids); // simpler than the loop
console.log('promise'+i+' end');
if (response.end === 'N'){
return activate(i+1);
}
});
}
}
Don't forget the returns so that the promises chain together and you can wait for the end of the requests.

Related

Resolving a promise in Angular Service

We have a service, lets call it AccountService which exposes a method called getAccounts(customerId) among others.
In its implementation all it does is to fire up a $http GET request and return a promise to the calling controller which will put the returned array of accounts in the controller scope once resolved.
On a simplified view all looks like below:
// The service
.factory('AccountService', ['$http', function($http) {
var _getAccounts = function(customerId) {
var request = {
'method': 'GET',
'url': 'http://localhost:8081/accounts/' + customerId
};
return $(request);
};
return {
getAccounts: _getAccounts
};
}]);
// Inside the conntroller
AccountService.getAccounts($scope.customerId)
.then(function(response) {
$scope.accounts = response.data;
});
So once the promise will resolve the controller scope will get populated with the list of accounts.
Note that I kept the above code as simple as I could to get you the idea of what my problem is but in reality it will be code to deal with exceptions, watcher to refresh, etc. Everything works fine.
My problem is that this AccountService is used from lots of controllers and putting the promise resolve in all of these looks to me not only repeating all this boiler plate resolver code but also complicating the unit testing as I am obliged to r/test both successful and exception scenarios in every single controller test.
So my question is:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
Please note I am a very beginner with Angular and JS so please be gentle if my question looks naive. I have heaps of java experience and my mind seems to go java like everywhere which may not be the case.
Thank you in advance for your inputs
To answer your original question:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
In my opinion, no, there isn't. It boils down to the way asynchronous calls work - you either pass a callback (and the method returns nothing), or you don't pass a callback and the method returns an object which will be notified (a promise). There may be some workarounds, but I don't think it gets nicer than that.
One way to partially reduce the boilerplate is to use a catch in the service, and return the promise returned by it instead.
Consider the following extremely simplified example:
angular.module('myApp')
.factory('NetworkRequests', [
function() {
var _getData = function() {
var promise = new Promise((resolve, reject) => {
var a = true,
data = ['a', 'b', 'c'];
if (a) {
resolve(data);
} else {
reject('Rejection reason: ...');
}
});
return promise.catch((error) => {
// Notify some error handling service etc.
console.log(error);
return [];
});
};
return {
getData: _getData
};
}
]);
The promise variable would be the result from your http request. You should return some data in the catch function that makes sense in the controller context (e.g. empty array). Then you don't have to bother with error handling in the controller:
angular.module('myApp')
.controller('DataController', ['NetworkRequests',
function(NetworkRequests) {
NetworkRequests.getData().then((data) => {
this.data = data;
});
}
]);
Again, this doesn't solve the complete issue, but at least the error handling part can be encapsulated in the service.
You can design in such a way that once your $http is done with fetching the data, store it your factory variable (somewhat a cache), and for subsequent factory calls, you check if the cache has such data. If yes, return the cache data, else call the $http calls.
Here is the code:
.factory('AccountService', ['$http', '$q', function($http, $q) {
var cachedData = null;
var defered = $q.defer(); //create our own defered object
var _getAccounts = function(customerId) {
if (cachedData !== null) {
console.log('get from cachedData')
defered.resolve(cachedData); // resolve it so that the data is passed outside
return defered.promise; //return your own promise if cached data is found
} else {
var request = {
'method': 'GET',
'url': 'mockdata.json'
};
return $http(request).then((response) => { //return a normal $http promise if it is not.
console.log('get from $http');
cachedData = response.data;
return cachedData;
});
}
};
return {
getAccounts: _getAccounts
};
}]);
Here is the working plnkr. You can open up the console, and click the GetData button. You will see that first time it logs get from $http, where as subsequent calls it logs get from cachedData.
One way is to reuse an object and fill it with data. It is used by ngResource.
It is something like
var data = [];
function getAccounts(customerId) {
var promise = $http(...).then((response) => {
Object.assign(promise.data, response.data)
});
promise.data = [];
return promise;
};
Data is available for binding as $scope.accounts = AccountService.getAccounts(...).data. The obvious drawback is that there is a splash of unloaded content.
Another way is the one you've mentioned. It is being used most frequently. If there is a problem with WET code in controllers, it should be treated by eliminating WET code with class inheritance, not by changing the way it works.
Yet another way is the recommended one. Using a router and route/state resolvers eliminates the need for asynchronously loaded data. The data resolved in resolver is injected into route template as an array.

AngularJs - Share data across multiple functions inside factory

I am just starting to learn Angularjs so i might be using the whole thing wrong but please correct me.
I have the following factory that goes like this:
app.factory('MenuService',['service1','service2','service3',function(service1,service2,service3){
var var1 = [],
var2 = [],
var3 = [];
service1.getDataMethod(function(data){
// processes data and saves it in var1
});
service2.getDataMethod2(function(data)){
});
/// same goes for service3.
return {"prop2": var1, "prop2" : var2, "prop3": var3};
}])
I need to process the data return by service2 , based on data returned in the first service but every time i try to access it, the variable is empty.
I know the functions return a promise, is there a way to tell the second function to wait for the first one to finish in order to be able to use the data it brings?
I hope i made myself understood. Let me know if i should add something else.
Like this?
service1.getDataMethod(function(data){
// processes data and saves it in var1
}).then(function(data) {
return service2.getDataMethod2(data)
})
Basically each promise have .then() method. So you can chain this as much as you need a.then().then().then()
In addition, some promise implementations have method wrappers such as .success() and .error(). These methods works similar to .then()
in angular you have access to $q service that is implementation of q library that works with promises. https://docs.angularjs.org/api/ng/service/$q
If you don't like it for some reason try async
Basically you have to chain your promises:
service1.getDataMethod(function(data){
return something; // this is important, only that way you will be
// able to use the next .then()
})
.then(function(something) { // what you returned previously will be the function's argument
return service2.getDataMethod2(data);
})
.then(function(result) { // `result` is what getDataMethod2 resolves
})
.catch(function(err) {
// very important to add .catch to handle errors
});

Is it possible to create a promise loop until reject in angular

I wish to load ~10000 resources and doing this all at once during the resolve phase takes a bit too long due to certain calculations being done. So then I came to the idea to load the resources page by page sequentially, however since all these resources need to be visible (on a map) standard, user-input based pagination, doesn't really work.
I know that promises can be chained like:
promise.then(doThis).then(doThat).then(doWhat);
And I know that an array of promises can be resolved with $q.all like:
$q.all([doThis, doThat, doWhat]);
However what I want to is to call the same promise again and again in series until I hit a rejection.
Example:
function next() {
var deferred = $q.defer();
if(someCondition) {
deferred.reject();
} else {
//do something
//store data somewhere
deferred.resolve();
}
return deferred.promise;
}
Let's say that this function does some $http calls and stores the result somewhere in the service/controller. If it hits a certain condition (perhaps there aren't any pages anymore or an http error) it rejects a promise, otherwise it resolves it.
Now I'd like to do something like this pseudocode
$q.loop(next).untilError(handleError);
Where next will be called in a loop upon resolving the previous next call, until rejection.
Is something like this possible?
Check the console of this demo: JSFiddle.
It ensures the calling of the APIs are using userId from 1 to 5 sequentially. And stop at some condition (userId > 5).
angular.module('Joy', [])
.controller('JoyCtrl', ['$scope', '$http', function ($scope, $http) {
getUser(1, getUser);
function getUser(userId, next) {
if (userId > 5) {
console.log('Enough. Stop');
return;
}
$http.get('http://jsonplaceholder.typicode.com/posts?userId=' + userId).then(function (data) {
console.log(data.data);
next(userId + 1, next);
});
}
}]);

Multiple HTTP requests in Angular

So this my controller:
app.controller('dbCtrl', function($scope, $http) {
$http.get("http://private-abc.apiary-mock.com/bus")
.success(function(response) {
$scope.network = response.networkupdates;});
});
What I wanted to do next is call a 2nd HTTP request, I guess in terms of best practice would it be best to create a 2nd controller to call the 2nd HTTP or would it be best to include the 2nd HTTP call in this current controller (and if so, how?)
Thanks.
So one of the cool aspects of using promises is that they can be chained. So in your case, you are calling:
$http.get("http://private-abc.apiary-mock.com/bus")
Which returns a promise that you can then chain to another promise like so:
var requests = $http.get("http://private-abc.apiary-mock.com/bus").then(function(response) {
$scope.network = response.networkupdates;
// make second get call and return it to chain the promises
return $http.get("some-other-endpoint").then(function(otherResponse) {
// you can do something here with the response data
return otherResponse;
});
});
What you have now is two chained promises that will return the final value, so if you call this later:
requests.then(function(otherResponse) {
// or you can do something here
doSomething(otherResponse);
});
As far as best practices for Angular, I would say you are better off creating a service or factory to handle any and all http requests. Controllers are really just meant to bind data to the view; services are where your business logic and data population should happen.
You can make your $http calls in the same controller.
The $http service is a function which takes a single argument — a configuration object — that is used to generate an HTTP request and returns a promise with two $http specific methods: success and error. It internally uses $q (a promise/deferred implementation inspired by Kris Kowal's Q).
If your two $http are independent of each other you use the $q.all to "join" the results of your http calls.
Example :
$q.all([
$http.get("http://private-abc.apiary-mock.com/bus"),
$http.get('/someUrl')
]).then(function(results) {
$scope.network = results[0];
$scope.whatevername= results[1]
});
}
If your http calls are dependent on one another then you can use the concept of chaining.
for example:
$http.get("http://private-abc.apiary-mock.com/bus").then(function(result) {
$scope.network = result.networkupdates;
return $http.get("someurl").then(function(res) {
return res;
});
});
For refernce of q you can see https://github.com/kriskowal/q
For refernce of $q service you can see https://docs.angularjs.org/api/ng/service/$q

Passing data between controllers in angular while waiting for promise [duplicate]

I want to implement a dynamic loading of a static resource in AngularJS using Promises. The problem: I have couple components on page which might (or not, depends which are displayed, thus dynamic) need to get a static resource from the server. Once loaded, it can be cached for the whole application life.
I have implemented this mechanism, but I'm new to Angular and Promises, and I want to make sure if this is a right solution \ approach.
var data = null;
var deferredLoadData = null;
function loadDataPromise() {
if (deferredLoadData !== null)
return deferredLoadData.promise;
deferredLoadData = $q.defer();
$http.get("data.json").then(function (res) {
data = res.data;
return deferredLoadData.resolve();
}, function (res) {
return deferredLoadData.reject();
});
return deferredLoadData.promise;
}
So, only one request is made, and all next calls to loadDataPromise() get back the first made promise. It seems to work for request that in the progress or one that already finished some time ago.
But is it a good solution to cache Promises?
Is this the right approach?
Yes. The use of memoisation on functions that return promises a common technique to avoid the repeated execution of asynchronous (and usually expensive) tasks. The promise makes the caching easy because one does not need to distinguish between ongoing and finished operations, they're both represented as (the same) promise for the result value.
Is this the right solution?
No. That global data variable and the resolution with undefined is not how promises are intended to work. Instead, fulfill the promise with the result data! It also makes coding a lot easier:
var dataPromise = null;
function getData() {
if (dataPromise == null)
dataPromise = $http.get("data.json").then(function (res) {
return res.data;
});
return dataPromise;
}
Then, instead of loadDataPromise().then(function() { /* use global */ data }) it is simply getData().then(function(data) { … }).
To further improve the pattern, you might want to hide dataPromise in a closure scope, and notice that you will need a lookup for different promises when getData takes a parameter (like the url).
For this task I created service called defer-cache-service which removes all this boiler plate code. It writted in Typescript, but you can grab compiled js file. Github source code.
Example:
function loadCached() {
return deferCacheService.getDeferred('cacke.key1', function () {
return $http.get("data.json");
});
}
and consume
loadCached().then(function(data) {
//...
});
One important thing to notice that if let's say two or more parts calling the the same loadDataPromise and at the same time, you must add this check
if (defer && defer.promise.$$state.status === 0) {
return defer.promise;
}
otherwise you will be doing duplicate calls to backend.
This design design pattern will cache whatever is returned the first time it runs , and return the cached thing every time it's called again.
const asyncTask = (cache => {
return function(){
// when called first time, put the promise in the "cache" variable
if( !cache ){
cache = new Promise(function(resolve, reject){
setTimeout(() => {
resolve('foo');
}, 2000);
});
}
return cache;
}
})();
asyncTask().then(console.log);
asyncTask().then(console.log);
Explanation:
Simply wrap your function with another self-invoking function which returns a function (your original async function), and the purpose of wrapper function is to provide encapsulating scope for a local variable cache, so that local variable is only accessible within the returned function of the wrapper function and has the exact same value every time asyncTask is called (other than the very first time)

Categories

Resources