I'm writing an app which
makes an AJAX call to establish user location based on an IP
address, then
makes a second AJAX call to provide current weather data
for this location, then
shows this data in DOM
I've developed the following promises chain which successfully delivers the above:
getUserLocation()
.then(function(locationData) {
return getCurrentWeather(locationData);
}).then(function(currentWeatherData) {
return showCurrentWeather(currentWeatherData);
});
Now I need to add a further functionality to the app so that it:
makes an AJAX call to establish user location based on an IP
address, then
makes a second AJAX call to provide current weather data
for this location, and
makes a third (concurrent, parallel) AJAX call to provide weather forecast data for this location, then
shows the current weather data in DOM, and
shows the weather forecast data in DOM
Below is what I've been experimenting with, but it does not work. Would you know how to fix this? My objective is to find a solution that makes only one AJAX call to get user location and then uses this data in two further AJAX calls (returned from the getCurrentWeather and getWeatherForecast functions).
getUserLocation()
.then(function(locationData) {
getCurrentWeather(locationData);
getWeatherForecast(locationData);
}).then(function() {
showCurrentWeather(currentWeatherData);
showWeatherForecast(weatherForecastData);
});
UPDATE 1 - WORKING SOLUTION FOUND:
Thanks to the approach suggested by #ReedSpool, I've managed to develop a working code:
getUserLocation()
.then(function(locationData) {
return jQuery.when(
getCurrentWeather(locationData),
getWeatherForecast(locationData)
);
}).then(function(currentWeatherData, weatherForecastData) {
showCurrentWeather(currentWeatherData[0]);
showWeatherForecast(weatherForecastData[0]);
});
The solution includes the jQuery.when. I also had to add index values of 0 to the arguments passed to the showCurrentWeather and showWeatherForecast functions. This is because the returned JSON now includes additional information about the readyState and status. This was not happening with my initial scenario with only one AJAX call to the weather API.
If you're indeed using jQuery promises (I'm gathering from the tags on your question, since the code provided has no jQuery specific stuff), then you're looking for jQuery.when() Every complete promise library has a similar function to this (Q.all for example)
Notice in your first example, you return the result of getCurrentWeather(locationData). Returning a promise inside a promise callback makes the outer promise wait for the inner promise to resolve. To wait for two or more promises to resolve, use jQuery.when() like...
getUserLocation()
.then(function(locationData) {
return jQuery.when(
getCurrentWeather(locationData),
getWeatherForecast(locationData)
);
}).then(function(curretnWeatherData, weatherForecastData) {
showCurrentWeather(curretnWeatherData[0]);
showWeatherForecast(weatherForecastData[0]);
});
See the documentation here: https://api.jquery.com/jquery.when/
Related
I'm working with AngularJS 1.5 (I'm really beginner with JS) in a user profile view where the user can change a lot of things in his data.
In this view the data is split in several sections and save all of this data means to do several calls to server ( due to the design of app). So the problem that I found is that when the user modify his data maybe modify only a part of this and when it push save button I don't want call all methods, only to necessary methods.
I've programed the way to detect the changes in data blocks when the user push the save button, sometimes the controller make a call and in other cases two or three. But the problem is that the calls (made with $resource library) is executed asyncronously and I would like can control this better.
I would like do the next: store all calls in a list or array but wihout execute them and after execute all at the same time (more or less). If any of this fails I would like show a error message to user (only a generic error message), but internally log the call that failed, and in the same way only show (and only one) a success message when all calls have ended with success ( and not a message per success call).
I don't know how to do this, some mates say me that maybe I need use $q AngularJS service to do this, or store the promises that $resource have to execute all after (I've trying this without success) or work with promises in JS.
Anyone can give me any idea?
Finally I resolved my problem using $q. At first I wanted store the calls without execute them (I thought that it was better) but finally I can check that only stored the results of this calls is enought for my aim. So, this a skeleton of the solution that I've been done:
At the beginning of the controller
var promises = [];
In all places where I need make a controlled call inside of save user data function:
var deferred = $q.defer();
var promise = vm.teacher.$update(
function () { // Success
deferred.resolve('Success updating the teacher.');
},
function (error) { // Fail
deferred.reject('Error updating the teacher, error: ' + error)
});
promises.push(deferred.promise)
}
...
... vm.otherItems.$update ...
...
And at the end of this function, something like this:
$q.all(promises).then(
function(value){
console.log('Resolving all promises, SUCCESS, value: ')
console.log(value);
toastService.showToast('Success updating the teacher.');
// It deleted old promises for next iteration (if it exists)
promises = [];
},function(reason){
console.log('Resolving all promises, FAIL, reason: ')
console.log(reason);
toastService.showToast('Fail updating the teacher.');
}
)
Thanks for the help!
Could someone explain to me what this code does? I know that it fetches countries and push them to the list which is shown in a web page, but why? I think that $scope.countries = service.query() is enough or is this way to avoid Asynchronous Problems?
$q.all([$scope.address.$promise, $scope.countrys.$promise]).then(function() {
if (!$scope.address.country.id) {
return $q.reject();
}
return Country.get({id : $scope.address.country.id}).$promise;
}).then(function(country) {
$scope.countrys.push(country);
});
Descriptions in code comments. This is conditionally data loading process using promise mechanism. Try to specify your question more detailed if is is not enough for you
// if address and countries was loaded (probably from ajax request)
$q.all([$scope.address.$promise, $scope.countrys.$promise]).then(function() {
// if address doesn't exist reject all actions
if (!$scope.address.country.id) {
return $q.reject();
}
// otherwise load country based on address field as a promise
return Country.get({id : $scope.address.country.id}).$promise;
}).then(function(country) {
// when loading process is finished add country to dataset
$scope.countrys.push(country);
});
According to $q service documentation
$q.all([promise1, promise2, ...])
Combines multiple promises into a single promise that is resolved when
all of the input promises are resolved.
service.query() will inevitably return a promise, which resolves when the asynchronous call completes. It would make no sense to set the scope property (which you will no doubt bind to some form of list) with a promise.
What the code is doing is waiting for the promise(s) to resolve, performing some logic, and setting the resulting data to a scope property.
I downloaded a library called jsdeferred to try and help me with some code-flow problems, but I am a little lost, as its examples and ...'documentation' is a little unclear on some things. But as I kept reading and digging, and of course googling everything under the sun, I also found out jQuery has its own Deferred() system. I am linking both here, for proper context.
Link to jsDeferred Library
Link to jQuery.Deferred()
The Problem
I need to find a way to tell the page to "hold on until the last thing is done".
This is what thought jsdeffered did. So part of my question is asking which should I use? jsDeferred or jQuery.Deferred(); and then how to use it as I've outlined below.
The Situation
My scenario is this, in a nutshell, I need to perform the following behavior.
page loads, a view model is defined
This is using kendo ui mvvm to declare my view model, so it is a kendo.data.ObservableObject
an $.ajax call is made to the database to get some default model data
This is where I am getting the most trouble. I need everything to "hold on" until this $.ajax is done. But I don't want to wrap everything in the $.ajax().done(r) if I can help it. That looks/feels very sloppy to me and is kind of confusing at times.
other widgets on the page are rendered, they have respective database queries done through kendo ui Remote DataSource.
These are actually working as intended.
jQuery Validate is wired to the view, with defaults having been set already.
This is also working as intended.
kendo.bind('body', viewModel); is called to perform model binding.
Now this is where I am running into trouble, going back to step 2 where I was making the $.ajax call. What keeps happening is that kendo.bind is fired before the $.ajax completes. I can put it in the $.ajax({}).done(); function, and for this exact one specific page that does work, but there will be many other situations where that isn't suitable.
What I have tried
First, I'll be clear that the jsdeferred documentation is very unclear to me, as running its samples verbatim doesn't actually work. I am continuously told that next is not defined and the like. I eventually figured out that you have to have an implicit Deferred. before you call next the first time.
So here is what I thought would happen...
var viewModel = new kendo.data.ObservableObject({
// various view model properties defined
});
Deferred.define();
next(function() { // let's call this STEP 1
$.ajax({
// data for ajax to controller
}).done(function(result) {
// perform operations with result
});
}).
next(function() { // let's call this STEP 2
$('#dropdownlist_target').kendoDropDownList({
// parameters, remote data source for drop down list, etc.
}).data("kendoDropDownList");
}).
next(function() { // let's call this STEP 3
$('form').validate({
// any extra form validation stuff
});
}).
next(function(){ // let's call this STEP 4
kendo.bind('body', viewModel);
});
I believed that these would each run one, right after the other, when the previous one is finished. But that is not what is happening. STEP 1 is still in the process of fetching while STEP 2, 3 and 4 are running.
This doesn't seem to be any different than the way the code was running without the jsdeferred library. So I am very confused and would absolutely love some help here. I need STEP 1 to be completely finished before STEP 2 fires, basically.
The problem is that next() expects you to return the thing you want it to wait for. In step one, you're not returning anything. jsdeferred is therefore assuming you were performing a synchronous operation (that has already finished), and so it continues with step 2.
Instead, return the jQuery.Deferred() returned from the $.ajax() call. jsdeferred will then wait for that to complete before it executes step 2.
Regardless of this, I'd dump jsdeferred. As you've realised, jQuery has a fully fledged Deferred implementation. I'm not sure what jsdeferred brings to the party.
Using $.ajax().done(r) is not sloppy. Asynchronous behaviour is the core of event driven languages, and JavaScript is one. Embrace it, or you'll go bald very early in life trying to avoid it.
If you revert to jQuery's Deferred implementation, you might like then(), to give you the semantics of next();
$.ajax({
// data for ajax to controller
}).done(function(result) {
// perform operations with result
}).then(function () {
$('#dropdownlist_target').kendoDropDownList({
// parameters, remote data source for drop down list, etc.
}).data("kendoDropDownList");
$('form').validate({
// any extra form validation stuff
});
kendo.bind('body', viewModel);
}).then(function () {
// Note you can chain then()'s as well.
});
You can just use the then method on your $.ajax() result in the same way you're using jsDeferred's next helper. Generally speaking, then is a more flexible method than done. And as Matt noted in his answer, it's a common mistake in promise based programming to forget to return a new promise within the handler, causing it to resolve prematurely with undefined instead of waiting on a the new promise.
$.ajax({ // let's call this STEP 1
// data for ajax to controller
}).
then(function(result) {
// perform operations with result
}).
then(function() { // let's call this STEP 2
$('#dropdownlist_target').kendoDropDownList({
// parameters, remote data source for drop down list, etc.
}).data("kendoDropDownList");
}).
then(function() { // let's call this STEP 3
$('form').validate({
// any extra form validation stuff
});
}).
done(function(){ // let's call this STEP 4
kendo.bind('body', viewModel);
});
Note that in my refactoring, all of those thens will execute immediately in a row, unless a new promise is returned. So you may as well combine them.
then takes a function that either returns a value or a promise, and it returns a new promise. If its function returned a value, the new promise is immediately resolved with that value. If its function returned a promise, then that promise is passed through as the new promise. Note that jQuery's then only works this way as of jQuery versions >=1.8.
I have a settings page on my jQuery mobile website When user clicks save button, I update the server for 3 different user inputs i.e language, currency, threshold
In order to do this I make 3 separate ajax calls (with PUT). So when all are succesfull I go to another page if any of those fails I stay on the same page and show the error messages.
The problem is I only want to switch the page if all calls are succesful, and if there are any errors I want to show one alert with all messages (rather than 3 separete alert windows), so I need to wait the results of all these calls.
To achive this in all 3 Ajax calls I used;
async:false
And I put a boolean in all these calls succes methods like;
success: function (data){
languageUpatesuccesful=true;
}
and then something like this;
if(languageUpatesuccesful){
make the next call to update currency..etc
}
...
if(allsuccesful(){
changepage();
}
So I can track when exactly when one calls finishes then I make the next call, if all succesful switch to another page.
While this works, I think this is a horrible solution, is there a way to achive this by using async:true ?
Because disabling asynchrous ajac freezes the page and I cant even show animations, also it is not recommended by jQuery. But then how can I know when these 3 calls are finished and take action depending on result?
Use deferred objects together with $.when:
$.when(ajax1(), ajax2(), ajax3()).done(function() {
// all Ajax calls successful, yay!
}).fail(function() {
// oh noes, at least one call failed!
});
Where ajaxX is defined as:
function ajax1() {
return $.ajax(...);
}
If you indeed want to chain the Ajax calls instead of having them concurrent, have a look at Bergi's solution.
You can easily chain them by using the Deferred interface:
$.ajax({…})
.then(function(languageUpateResult) {
return $.ajax({…});
})
.then(function(currencyUpdateResult) {
return $.ajax({…});
})
.then(function(thresholdUpdateResult) {
changePage();
});
Sorry, I skipped the fact that the ajax calls are separate. The above code executes them sequentially, if you just want to execute them in parallel and wait for all of them to finish use $.when() - see #FelixKling's answer.
You can try using web workers to do your async calls in a background thread. Web workers have pretty good browser support and should solve your issue. If you are interested, I have written a little abstraction of the web worker library to make them easier to work with (just send me a pm).
I am not having any issues with logging in or even calling the api, I just have an issue with getting the response outside of the api callback. I know that it runs asynchronously so I would like to put it in a function that would return the response. Here is my idea
//What I would like to be able to do
function fbUser(){
FB.api('/me', function(response){
//this logs the correct object
console.log(response);
});
//How do I get the response out here?
return response;
}
I would like to call the /me api function once in the beginning and then pass it around to my view objects (I just use the response inside of Backbone Views) and depending on what is needed make other api calls. I currently have certain things working by calling the view from inside of the callback
//What I am doing now, but I lose the ability to pass anything other than the
//the current response to this function/View
FB.api('/me', function(response){
var newView = new facebookView({model: response});
});
I orginally was trying this, but because the api call is asynchronous I had issues with things being undefined
//What I started with but had async issues
var fbResponse;
FB.api('/me', function(response){
fbResponse = response;
});
//I would then try and use fbResponse but it would be undefined
I lose the first response when I make the second. For example my first api call is to /me to get the user info. I can then call /your-fb-id/photos and get photos, but if I make the call to another function inside of the photo api callback I only can reference that response I lost the original /me response. If I could get the response out of the callback then I would be able to pass it as needed. I understand that response is only valid inside of the callback, so how do I make it valid outside of the callback while taking into account it's asynchronousness?
OK everyone I figured this out. It took me a long time and reading many different pages. What I said I wanted to do all deals with callbacks and closures. I will first cover the callback issue. Because the FB.api function is asynchronous you never know when it will return. You can stop Javascript or set a timer, but that is a horrible way to do this. You need a callback. In fact the FB.api is using a callback. That's what the anonymous function as the second parameter is. What I did was create another function that called fbUser and used a callback. Here is what I did:
function startThis() {
var getUser = fbUser(function(model){
console.log(model);
startapp(model);
});
};
function fbUser(callback){
FB.api('/me', function(response){
callback(response);
});
}
The startThis function is called on a positive Facebook auth response. It then calls the fbUser function which has a callback. The callback returns the callback from the FB.api function. Because startThis uses that callback with the return value being called model, the other code will not execute until the callback returns. No more undefined issues. These functions are just wrappers to get the Facebook response to my views. I may have added one too many layers of abstraction, but if you want to pass around the response this is the way.
Secondly I wanted to pass this response on to another view. For example one view loads basic info (using the response from fbUser). I now want to pass that to another view that loads photos (I know this is not best practices MVC, but by using Facebook I don't have much control over the Model). The problem I had though was I couldn't pass the original response to the next view because inside of the callback function for the FB.api call this refers to the Window and not the object I was in. Solution: closures. I won't explain this perfectly, but a closure is a local variable inside of a function that still has a reference inside of an anonymous function. Here is my solution which should illustrate what I am talking about:
photos: function(){
var This = this;
var apiString = '/' + this.model.id + '/photos';
FB.api(apiString, function(response){
loadPhoto(response, 1, This.model);
});
The function loadPhoto is a wrapper to load a photo view (I know backbone can help me with loading different views, but I was tackling one problem at a time). It takes the photo api call as the model, a number as an offset, and the origanl response. The first line in this function sets this to a local variable This. That allows me inside of the anonymous callback function to reference the object this was called from.
I hope this can help someone as I spent a lot of hours and a lot of testing time to find the solution to this. If you don't know about how callbacks or closures work it is hard to find the information you are looking for.