I'm trying to rewrite the code for http://m.amsterdamfoodie.nl in a more modern style. Basically single page Angular app downloads a set of restaurants with locations and places them on a map. If the user is the Amsterdam area then the user's location is added too, as are the distances to places.
At present I manage the asynchronous returns using a lot of if (relevant object from other async call exists) then do next step. I'd like to make more use of promises would be better.
So, flow control should be:
Start ajax data download, and geolocation call
if geolocation returns first, store coords for later
once ajax data is downloaded
if geolocation available
calculate distances to each restaurant, and pass control to rendering code
else pass control immediately to render code
if geolocation resolves later, calculate distances and re-render
The patterns I find on the internet assume that all async calls must return successfully before continuing, whereas my geolocation call can fail (or return a location far from amsterdam) and that's OK. Is there a trick I could use in this scenario or are the conditional statements really the way to go?
Every time you use .then, you essentially create a new promise based on the previous promise and its state. You can use that to your advantage (and you should).
You can do something along the lines of:
function getGeolocation() {
return $http.get('/someurl').then(
function resolveHandler(response) {
// $http.X resolves with a HTTP response object.
// The JSON data is on its `data` attribute
var data = response.data;
// Check if the data is valid (with some other function).
// By this, I mean e.g. checking if it is "far from amsterdam",
// as you have described that as a possible error case
if(isValid(data)) {
return data;
}
else {
return null;
}
},
function rejectHandler() {
// Handle the retrieval failure by explicitly returning a value
// from the rejection handler. Null is arbitrarily chosen here because it
// is a falsy value. See the last code snippet for the use of this
return null;
}
);
}
function getData() {
return $http.get('/dataurl').then(...);
}
and then use $q.all on both promises, which in turn creates a new promise that resolves as soon as all the given promises have resolved.
Note: In Kris Kowal's Q, which Angular's $q service is based on, you could use the allSettled method, which does almost the same as all, but resolves when all promises are settled (fulfilled or rejected), and not only if all promises are fulfilled. Angular's $q does not provide this method, so you can instead work your way around this by explicitly making the failed http request resolve anyways.
So, then you can do something like:
$q.all([getData(), getGeolocation()])
.then(function(data, geolocation) {
// `data` is the value that getData() resolved with,
// `geolocation` is the value that getGeolocation() resolved with.
// Check the documentation on `$q.all` for this.
if(geolocation) {
// Yay, the geolocation data is available and valid, do something
}
// Handle the rest of the data
});
Maybe I'm missing something... but since you have no dependencies between the two async calls, I don't see why you can't just follow the logic you outlined:
var geoCoordinates = null;
var restaurants = null;
var distances = null;
getRestaurantData()
.then(function(data){
restaurants = data;
if (geoCoordinates) {
distances = calculate(restaurants, geoCoordinates);
}
// set $scope variables as needed
});
getGeoLocation()
.then(function(data){
geoCoordinates = data;
if (restaurants){
distances = calculate(restaurants, geoCoordinates)
}
// set $scope variables as needed
});
Related
I have a very large angularjs app, that sells stuff and has filters
It seems that we need to support people on flaky connection.
That means that if user selects 'used product' filter and then he unselects 'used product', there will be a 2 calls to the server via $http.
$http.get("reloadresults?used=true", function (response) { $scope.items = response items; }); at 12:03:04 hours
$http.get("reloadresults?used=false", function (response) { $scope.items = response items; }); at 12:03:05
Now, image there is a bottleneck or something and the first call with 'used=true' returns last, then there is a problem with the filters.
I know there is a $http interceptor in angularjs, based on promises, how would i fix this problem? So that requests are processed in the order they are sent, meaning 'used=true' and only then used=false.
Edit: cant block thread, cant refactor, just need for the promises to fullfil in the order they were first sent. I think ill post answer later.
I din't understand your question well but i think you are looking for
$q.all(valid_request)
You could indeed ensure that success handlers are called in the correct order by forming a queue (a promise chain) however it is simpler, and better in this case, to nullify the previous request each time a new request is made.
There's a number of ways in which this could be achieved. Here's one ...
function cancelPrevious(fn) {
var reject_ = null;
return function(x) {
if(reject_) reject_(new Error('cancelled'));
return Promise.race(fn(x), new Promise(function(_, reject) {
reject_ = reject; // if reject_ is called before fn(x) settles, the returned promise will be rejected.
}));
};
}
which allows you to write ...
var get = cancelPrevious(function(str) {
return $http.get(str);
});
... and to make requests from any number of event threads :
get('reloadresults?used=true').then(function(response) {
// This function will be reached only if
// $http.get('reloadresults?used=true') fulfills
// before another get() call is made.
$scope.items = response.items;
});
...
// This call causes the then callback above to be suppressed if not already called
get('reloadresults?used=false').then(function(response) {
$scope.items = response.items;
});
Notes :
http requests are not canceled. Instead, the succuss path stemming from each request is made "cancelable" by racing against a rejectable promise.
side-effects included in the function passed to cancelPrevious() may be executed; in general, don't include such side effects.
I'm testing a service that uses another service for API calls, let's call this the data service. The data service is tested elsewhere, so I've abstracted it away with a simple implementation that contains empty functions; I'm returning data via a deferred object and Jasmine's spyOn syntax.
The trouble I'm finding with this approach is when the data is returned, it's not immediately available on the calling object, as it would be if I used $httpBackend. Aware I could just use $httpBackend, but I'd like to know if I've missed something (simple or otherwise) in this approach.
Example section of code I'm trying to test:
storeTheData = dataService.getSomeData();
storeTheData.$promise.then(function(data) {
/*this would work*/
console.log(data);
/*but this would not, when testing using $q*/
_.forEach(storeTheData, function(storedData) {
/*do something with each object returned*/
});
});
As a side note, I don't think the situation is helped by the ...$promise.then on another line, but ideally I wouldn't change the code (I'm providing test coverage to something written a while ago...)
Example of the test:
beforeEach(
...
dataService = {
getSomeData: function () { }
};
getSomeDataDeferred = $q.defer();
spyOn(dataService, "getSomeData").and.returnValue({$promise: getSomeDataDeferred.promise});
...
);
it(...
getSomeDataDeferred.resolve([{obj: "obj1"}, {obj: "obj2"}]);
$scope.$apply();
...
);
With the test described above, the console.log(data) would be testable as the data is accessible from being passed into the .then(). But the data is not immediately available from storeTheData, so storeTheData[0].obj would be undefined. On debug, I can see the data if I go through the promise that was attached to storeTheData via storeTheData.$$state.value
Like I said, I know I could use $httpBackend instead, but is there any way to do this with $q without changing the code under test?
I've not found a way to do this with $q.resolve, but I do have a solution that doesn't involve using the data service or changing the code under test. This is as good, because the main things I wanted to avoid were testing the data service as a side effect and changing the code.
My solution was to create a $resource object via $injector...
$resource = $inject.get("$resource");
...then return that in my basic implementation of the data service. This means I could use $httpBackend to respond to the request to an end point that isn't reliant on the data service's definition staying consistent.
dataService = {
getSomeData: function () {
/* new code starts here */
var resource = $resource(null, null, {
get: {
method: "GET",
isArray: true,
url: "/getSomeData"
}
});
return resource.get();
/* new code ends here */
}
};
...
$httpBackend.when("GET", "/getSomeData").respond(...;
In my application, I am storing data in local storage and trigger async http post in the background. Once successfully posted, the posted data gets removed from local storage. When http post is in process, there may be more data added to local storage so I need to queue up the post and sequentially process it because, I need to wait for the local storage to be cleared from the successful posts. The task should be called recursively until there is data in local storage.
taskQueue: function () {
var deferred = $q.defer();
queue.push(deferred);
var promise = deferred.promise;
if (!saveInProgress) {
// get post data from storage
var promises = queue.map(function(){$http.post(<post url>, <post data>).then(function(result){
// clear successful data
deferred.resolve(result);
}, function(error){
deferred.reject(error);
})
})
return $q.all(promises);
}
As angular newbie, I am having problems with the above code not being sequential. How can I achieve what I intend to? The queue length is unknown and also the queue length increases as the process is in progress. Please point me to other answers if this is a duplicate.
Async.js sounds a good idea but if you don't want to use a library ...
$q.all will batch up an array of requests and fire them at the same time and will resolve when ALL promises in the array resolve - WHICH IS NOT WHAT YOU WANT.
to make $http calls SEQUENTIALLY from an array do this ....
var request0 = $http.get('/my/path0');
var request1 = $http.post('/my/path1', {data:'fred'});
var request2 = $http.get('/my/path2');
var requestArray = [];
then ...
requestArray.push(request0);
requestArray.push(request1);
requestArray.push(request2);
then ...
requestArray[0].then(function(response0) {
// do something with response0
return requestArray[1];
}).then(function(response1) {
// do something with response1
return requestArray[2];
}).then(function(response2) {
// do something with response2
}).catch(function(failedResponse) {
console.log("i will be displayed when a request fails (if ever)", failedResponse)
});
While having a library solution would be great (per #nstoitsev's answer), you can do this without it.
sequential requests of unknown length
Just to recap:
we do not know the number of requests
each response may enqueue another request
A few assumptions:
all requests will be working on a common data object (local storage in your case)
all requests are promises
running the queue
function postMyData (data){
return $http.post(<url>, data)
}
var rqsts = []
function executeQueue (){
if(!rqsts.length)
//we're done
return
var rqst = rqsts.shift()
rqst()
.then(function(rsp){
//based on how you're determining if you need to do another request...
if(keepGoing)
rqsts.push(postMyData(<more data>))
})
}
codepen - http://codepen.io/jusopi/pen/VaYRXR?editors=1010
I intentionally left this vague because I don't understand what the conditions for failure are and if you wanted to vary up the requests to use more than the same $http.post call, you could pass it back in some way.
and just a suggestion
As angular newbie...
Many things are progressing towards this whole functional, reactive programming paradigm. Since you're relatively new to Angular and NG2 already has some of this built in, it might be worthy of your attention. I think rxjs is already in many NG2 example bundles.
The easies way to achieve this is by using Async.js. There you can find a method called mapSeries. You can run it over the queue and it will sequentially process all elements of the array one by one, and will continue to the next element only when the correct callback is called.
I'm trying to make a call to a function 'submittoServer' which is inside factory 'pService', which makes $http call and then broadcast the data. The call to the 'submittoserver' is happening inside a for loop. Problem here is that I couldn't see the actual call is being made until the last loop, which is sending only the last item, but as you see in the code below I want to update one particular variable after every call, can someone please suggest how can I don't that. I can't do call back here as I've other method that call this same factory function with different inputs.
for (var i = vr.lines.length - 1; i >= 0; i--) {
if (parseInt(vr.lines[i].id) === id && Boolean(vr.lines[i].IsVoided) != true) {
lineId = vr.lines[i].lineID;
pService.submitToServer(actionId, { "IData": id }, ineId)
linesRemoved = linesRemoved + 1;
}
if (linesRemoved === lineqty)
{
updateModel = true;
}
}
The problem here is that your service is a promise to return data. Your function will keep looping and running before the promise is resolved. You need to refactor your loop to take this into account.
Either add a .then(fn(){}) to handle the resolve promise. Gather up all the changed lineIds and submit them all at once and (again) handle the resolved promise with a .then(fn(){}). Lastly, given you next set of code logic, you probably want to do something more like $q.all to wait on all promise(s) to resolve before moving forward (see Wait for all promises to resolve)
Example
before your for loop:
var self=this;
self.linesRemoved = 0; // or init as needed.
Inside your for loop.
pService.submitToServer(actionId,{data}).then(function(resp){
self.linesRemoved++; // shortcut, this does +1 to itself.
});
Why do you have update model? With Angular your data is two-way bound and should just react to it being changed.
Sample $http call with return in a service, this is a promise itself:
return $http.post(url, data, { cache: true });
Use this in a controller like
service.callHttp(data).success(function(resp){
self.linesRemoved++;
}).error(function(resp){});
If you have to wait it might be better to hold all and wait until they are all finished.
var promises =[];
for(){
promises.push(service.callHttp());
}
$q.all(promises).then(function(){
// do work if(self.linesRemoved==lineQty)
// update... You can't evaluate until they are all finished right?
});
I'm sending a model to server via $http.post, but, say, empty dates must be deleted, ids must be converted to int, in float values comma must be replaced with dot. These are restrictions of the server-side json api, so I'm looking for a way to modify $http request. Complicated part is that the modification rules depend on a api method name, which itself is a part of request.
The most straightforward way is to declare a modifying function and pass model to that function right before $http.post
$scope.method1Adapter = function(model) {
var data = angular.copy(model);
// 30 lines of modification code
return data;
};
$http.post("/api", {method: "method1", "data": $scope.method1Adapter($scope.data)})
but I think it's a little bit spaghettysh.
Better way is a factory that gets a method name and returns an adapter callback.
coreApp.factory("httpAdapter", function() {
return {
get: function (method) {
if (method == 'method1') {
return function (model) {
var data = angular.copy(model);
// modifications
return data;
}
} else {
return function (model) {
return model;
}
}
}
}
});
so i can add this to $httpProvider.defaults.transformRequest callbacks chain
coreApp.config(function($httpProvider, httpAdapterProvider) {
$httpProvider.defaults.transformRequest.unshift(function(post) {
if (post && post.data && post.data) {
post.data = httpAdapterProvider.$get().get(post.method)(post.method);
}
})
});
And still I don't like that, because api for my application has 16 methods, and it would require 5 adapters which is about 100 lines of code hard to maintain.
Any ideas about more clean and neat solution? Thank you.
I wouldn't chain adapters here because, as you said, it would be hard to maintain.
My advice would be to use the $http interceptors (not the responseInterceptors, which are deprecated, but the normal one, see http://docs.angularjs.org/api/ng.$http).
Notice in that you have access to the "config" object that has the request url, amongst other interesting properties.
It won't be superneat but at least the problem can be contained in one isolated part of your codebase.