I have a button that I want to track when the user press it, and if the tracking ajax call returns data, then execute a promise, when thats all done, continue with the button event.
Below is what I have so far, however the button event and the 2nd promise fire at the same time. The button event isn't waiting for the 2nd promise to resolve
Click button
AJAX call records the event
If AJAX doesn't return a question then alert hello.
If the AJAX call returns a question, then build and open modal, when modal closes, then alert hello.
$('.my_button').on('click', function() {
ui_tracking('button_1').then(function () {
alert('Hello');
});
});
function ui_tracking(type, payload) {
var deferred = $.Deferred();
var log_action = function () {
$.ajax({
url: '/api/submit_action',
type: 'POST',
dataType: 'json',
data: {
type: type,
payload: JSON.stringify(payload)
},
success: function(results, textStatus, xhr) {
if (typeof(results) !== 'undefined' && results !== null) {
if (typeof(results.data) !== 'undefined' && results.data !== null) {
if (results.data.question) {
startQuestion(results.data.question).then(function () {
deferred.resolve();
}, function() {
deferred.reject();
});
}
}
}
deferred.resolve();
},
error: function (xhr, textStatus, errorThrown) {
deferred.reject();
}
});
};
log_action();
return deferred.promise();
}
function startQuestion(question_data) {
var deferred = $.Deferred();
var openQuestion = function () {
$('#question-modal .modal-body .question').html(question_data.question).attr('data-question-id', question_data.id);
$('#question-modal').modal('show').on('hidden.bs.modal', function (e) {
deferred.resolve();
$('#question-modal').unbind('hidden.bs.modal');
});
};
openQuestion();
return deferred.promise();
}
sf
your success: callback calls deferred.resolve(); straight away (at the end, after the if condition)
success: function(results, textStatus, xhr) {
if (typeof(results) !== 'undefined' && results !== null) {
if (typeof(results.data) !== 'undefined' && results.data !== null) {
if (results.data.question) {
startQuestion(results.data.question).then(function () {
deferred.resolve();
}, function() {
deferred.reject();
});
}
}
}
// this gets called regardless of the above conditions!
deferred.resolve();
Knowing $.ajax returns a (jQuery) Promise, I believe you could simplify ui_tracking function as follows:
function ui_tracking(type, payload) {
return $.ajax({
url: '/api/submit_action',
type: 'POST',
dataType: 'json',
data: {
type: type,
payload: JSON.stringify(payload)
}
}).then(function(results) {
if (results && results.data && results.data.question) {
return startQuestion(results.data.question);
}
});
}
In the above .then, if the conditions are not met, the return is undefined ... basically the same thing as you were doing with deferred.resolve() - the returned Promise will be resolved to undefined once the ajax completes
However, if the conditions are all met, the return is the Promise returned by startQuestion - which will mean the returned Promise will be that which is returned by startQuestion - therefore your code will wait on that promise to resolve before continuing
Also, no need for error handling that simply returns a rejected promise - let the outer call handle errors
Alternatively, you could write the whole lot as
$('.my_button').on('click', function() {
ui_tracking('button_1')
.then(startQuestion)
.then(function () {
alert('Hello');
});
});
function ui_tracking(type, payload) {
return $.ajax({
url: '/api/submit_action',
type: 'POST',
dataType: 'json',
data: {
type: type,
payload: JSON.stringify(payload)
}
});
}
function startQuestion(results) {
var deferred;
var openQuestion = function (question_data) {
$('#question-modal .modal-body .question').html(question_data.question).attr('data-question-id', question_data.id);
$('#question-modal').modal('show').on('hidden.bs.modal', function (e) {
deferred.resolve();
$('#question-modal').unbind('hidden.bs.modal');
});
};
if (results && results.data && results.data.question) {
deferred = $.Deferred();
openQuestion(results.data.question);
return deferred.promise();
}
}
Sure the logic has moved about, so maybe not exactly what you'd like
You can encapsulate each "step" in a function returning a promise (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) and then "chain" them.
This is a great post on executing promises in a series: http://www.datchley.name/promise-patterns-anti-patterns/#executingpromisesinseries (you may need a polyfill for your promising: https://github.com/stefanpenner/es6-promise)
Related
I am doing a few recurring AJAX calls where I pass an array from the front-end to the back-end and whenever it comes back to the front-end, the array gets smaller (by 1) and ultimately it'll be empty, therefore my recursive calls will stop.
Here's my calls:
function download_required_files(demo_data) {
var ajaxsecurity = setup_page_params.ajax_nonce;
jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: ajaxsecurity,
content_install_request_data: JSON.stringify(demo_data),
},
success: function (response) {
console.log(response);
var data = response.data || false;
/**
* If no steps are left, meaning that all required files have been downloaded, proceed with the whole install process.
*/
if(!data.remaining_steps || !data.remaining_steps.length) {
return false;
}
if(data.can_continue !== 'yes') {
return false;
}
if(data.remaining_steps && data.remaining_steps.length) {
demo_data.steps_to_take = data.remaining_steps;
download_required_files(demo_data);
}
$('.demo-loader-content').fadeOut();
},
error: function (response) {
$('.demo-loader-content').fadeOut();
}
});
}
Assuming I have 2 steps to download files for, this download_required_files will run twice, then it'll be done, but if I do:
var download_process = download_required_files(demo_data) //Runs 2 times
download_process.done(function() { //Do stuff here once that function ran 2 times });
It gives me the: Cannot read property 'done' of undefined error and for good reason. That download_process is not a promise object for it to have that property, it's just...empty.
Where should I intervene in my download_required_files so that it signals to outside code that "Hey, in a promise environment, I'm done!"?
Although the result of the call to $.ajax is a jqXHR object, which is promise-like, for what you describe I think I'd go with your own native Promise (or Deferred if you prefer) to represent the overall recursive process:
function download_required_files(demo_data) {
return new Promise(function(resolve, reject) {
function worker() {
var ajaxsecurity = setup_page_params.ajax_nonce;
jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: ajaxsecurity,
content_install_request_data: JSON.stringify(demo_data),
},
success: function (response) {
console.log(response);
var data = response.data || false;
/**
* If no steps are left, meaning that all required files have been downloaded, proceed with the whole install process.
*/
if(!data.remaining_steps || !data.remaining_steps.length) {
// *** All done
$('.demo-loader-content').fadeOut();
resolve();
} else if(data.can_continue !== 'yes') {
// *** All done; but is this an error condition? If so
// use `reject` instead of `resolve` below.
$('.demo-loader-content').fadeOut();
resolve();
} else {
demo_data.steps_to_take = data.remaining_steps;
worker(); // This is the internal recursive call
}
},
error: function (response) {
$('.demo-loader-content').fadeOut();
}
});
}
worker();
});
}
Or using Deferred instead:
function download_required_files(demo_data) {
var d = $.Deferred();
function worker() {
var ajaxsecurity = setup_page_params.ajax_nonce;
jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: ajaxsecurity,
content_install_request_data: JSON.stringify(demo_data),
},
success: function (response) {
console.log(response);
var data = response.data || false;
/**
* If no steps are left, meaning that all required files have been downloaded, proceed with the whole install process.
*/
if(!data.remaining_steps || !data.remaining_steps.length) {
// *** All done
$('.demo-loader-content').fadeOut();
d.resolve();
} else if(data.can_continue !== 'yes') {
// *** All done; but is this an error condition? If so
// use `d.reject` instead of `d.resolve` below.
$('.demo-loader-content').fadeOut();
d.resolve();
} else {
demo_data.steps_to_take = data.remaining_steps;
worker(); // This is the internal recursive call
}
},
error: function (response) {
$('.demo-loader-content').fadeOut();
}
});
}
worker();
return d.promise();
}
This would be my approach, separating the individual AJAX requests from the looping over the content, and that also from the DOM updates:
function download_one_file(demo_data) {
return jQuery.ajax({
url: ajaxurl,
type: 'POST',
dataType: 'json',
data: {
action: 'download_import_files_request',
security: setup_page_params.ajax_nonce,
content_install_request_data: JSON.stringify(demo_data),
}
});
}
function download_loop(demo_data) {
return download_one_file(demo_data).then(function(data) {
if (!data) {
return Promise.reject();
} else if (data.remaining_steps && data.remaining_steps.length) {
demo_data.steps_to_take = data.remaining_steps;
return download_loop(demo_data);
} else {
return Promise.resolve();
}
});
}
function download_required_files(demo_data) {
return download_loop(demo_data).finally(function() {
$('.demo-loader-content').fadeOut();
});
}
I have a function that has to connect to DB to get a upload token, then upload a file, then close the stream and log the file in DB. I'm having trouble chaining all this together.
var saveNewRequest = function (image_arr) {
return $.ajax({
url: 'http://' + AppVar.ServerUrlWithPort + '/restapi/MtReqNewRequest_SaveData',
type: 'POST',
data: JSON.stringify({
'SessionId': AppVar.SessionId,
'Name': $('#MtReqNewRequest_name').val(),
'Desc': $('#MtReqNewRequest_desc').val(),
'Obj': $('#MtReqNewRequest_obj').val(),
'Priority': $('#MtReqNewRequest_priority2').val(),
'Status': $('#MtReqNewRequest_status2').val(),
'Type': $('#MtReqNewRequest_type2').val()
}),
dataType: 'json',
contentType: "application/json",
timeout: 10000
}).done(function (response) {
if (response.ResultCode === '0') {
if (image_arr.length != 0) {
//this is recursively called upload function which returns jQuery promise (this works as intended)
// the promise resolves with RequestId which I need later on
return uploadImages(image_arr, image_arr.length, 0, response.RequestId)
} else {
//I would like this to return just this RequestId
Promise.resolve(response.RequestId)
}
} else {
Promise.reject().promise();
}
}).fail(function (x, t, m) {
if (t === "timeout") {
reject("Timeout: " + t);
} else {
reject($.i18n('Error-RetrivingDataProblem'));
}
})
}
And I call this in an event:
MtReq.saveNewRequest(image_arr).then(function (output) {
AppVar.nav.popPage().then(function () {
Utility.hideModalWithProgressBar();
if (!isNaN(output)) {
setTimeout(500, AppVar.nav.pushPage("MtReqRequestPage.html", { animation: "slide", id: output }));
}
})
}).catch(function (e) {
Utility.hideModalWithProgressBar();
ons.notification.alert(e);
})
I need to pass the RequestID to the AppVar.nav.pushPage, to open the page I just created. However, I'm getting whole response of the very first Ajax request in saveNewRequest.
This is Cordova app, using OnsenUI framework (but that's not relevant to the problem). Also, I'm using latest BluebirdJs as Promise polyfill (which to my knowledge should make JS and jQuery promises compatible).
Thanks for any help!
Substitute .then() for .done(); .done() returns same jQuery promise object returned by $.ajax(). return the Promise or other value from .then().
var saveNewRequest = function (image_arr) {
return $.ajax({
url: 'http://' + AppVar.ServerUrlWithPort + '/restapi/MtReqNewRequest_SaveData',
type: 'POST',
data: JSON.stringify({
'SessionId': AppVar.SessionId,
'Name': $('#MtReqNewRequest_name').val(),
'Desc': $('#MtReqNewRequest_desc').val(),
'Obj': $('#MtReqNewRequest_obj').val(),
'Priority': $('#MtReqNewRequest_priority2').val(),
'Status': $('#MtReqNewRequest_status2').val(),
'Type': $('#MtReqNewRequest_type2').val()
}),
dataType: 'json',
contentType: "application/json",
timeout: 10000
}).then(function (response) {
if (response.ResultCode === '0') {
if (image_arr.length != 0) {
//this is recursively called upload function which returns jQuery promise (this works as intended)
// the promise resolves with RequestId which I need later on
return uploadImages(image_arr, image_arr.length, 0, response.RequestId)
} else {
//I would like this to return just this RequestId
// added `return`
return Promise.resolve(response.RequestId)
}
} else {
// note `return`, removed `.promise()`
return Promise.reject()
}
}).fail(function (x, t, m) {
if (t === "timeout") {
// included `Promise`, chain `.reject()`
// note, `return`
return Promise.reject("Timeout: " + t);
} else {
// note `Promise.reject()`, added `return`
return Promise.reject($.i18n('Error-RetrivingDataProblem'));
}
})
}
I'm currently looping through a number of functions that validate particular input values. One in particular requires an ajax call to validate an address. I saw that users here on SO suggested using callbacks to return the value.
The only problem is, by the time it does retrieve the value, the function had already fired within the loop and returned an undefined value. I've been searching around and wasn't sure what the best solution would be. Do I somehow delay the loop? Do I have my function setup right in the first place? Need some serious help.
var validations = [validateUsername, validatePassword, validateAddress];
function submitForm() {
var inputs = validations.map(function(validation) {
return validation();
});
return inputs.every(function(input) {
return input === true;
}); // [true, true, undefined]
}
function validateAddress() {
function addressIsValid(callback) {
$.ajax({
type: 'POST',
url: '/address/validate',
data: $form.serialize(),
dataType: 'json',
success: function(response) {
return callback(response.Data === 200);
}
});
}
function callback(response) {
return response;
}
return addressIsValid(callback);
}
You should use Promises.
First, you should make your asynchronous functions return a Promise:
function validateAddress() {
return new Promise((resolve, reject)=> {
$.ajax({
type: 'POST',
url: '/address/validate',
data: $form.serialize(),
dataType: 'json',
success: response=> {
response.Data === 200 ? resolve() : reject();
}
});
});
}
Then rewrite your submitForm function like this:
function submitForm() {
var promises = validations.map(validation=> validation());
return Promise.all(promises)
}
Then you can use submitForm like this:
submitForm()
.then(()=> {
// form is valid
}, ()=> {
// form is invalid
})
I am writing an angular service to work with SharePoint data and I have run into a problem. I have a function in my service that updates and single item and returns an $http promise which works fine. The problem is I am trying to write a function now that utilizes the first function to loop and update multiple items. I want it to return a single promise once all items have been updated and it should reject if any of the items being updated failed. Here is the function:
this.UpdateListItems = function (webUrl, listName, itemsJson) {
if (numItems == -1) {
numItems = itemsJson.length;
c = 0;
f = 0;
}
var promises = [];
itemsJson.forEach(function (itemProps) {
var itemPromise = this.UpdateListItem(webUrl, listName, itemProps.Id, itemProps)
.then(function (response) {
c++;
if (c == numItems && f == 0) {
numItems = -1;
return itemsJson[listName];
}
}, function (error) {
c++; f++;
alert("ERROR!");//This gets called first alert below
if (c == numItems) {
numItems = -1;
return $q.reject(error);
}
});
promises.push(itemPromise.$promise)
}, this);
return $q.all(promises)
.then(function (data) {
alert("IN SUCCESS"); //This always gets called immediately after first item success instead of waiting for all items to finish
}, function (error) {
alert("IN ERROR"); //This never gets called
});
};
The $q.all is returning immediately after the first item returns successfully instead of waiting for the rest of the async item calls. Any help is much appreciated, I am new to all this. Thanks!
EDIT: Adding UpdateListItem code as requested:
this.UpdateListItem = function (webUrl, listName, itemId, itemProperties) {
if (typeof lists[listName] === 'undefined') {
lists[listName] = [];
}
var post = angular.copy(itemProperties);
DataUtilitySvc.ConvertDatesJson(post);
return this.GetListItemById(webUrl, listName, itemId)
.then(function (item) {
return $http({
url: item.__metadata.uri,
method: 'POST',
contentType: 'application/json',
processData: false,
headers: {
"Accept": "application/json;odata=verbose",
"X-HTTP-Method": "MERGE",
"If-Match": item.__metadata.etag
},
data: JSON.stringify(post),
dataType: "json",
}).then(function (response) {
var temp = [];
temp.push(itemProperties);
DataUtilitySvc.MergeByProperty(lists[listName], temp, 'Id');
return response;
}, function (error) {
return $q.reject(error);
});
}, function (error) {
return $q.reject(error);
});
};
Seems like this.UpdateListItem function already returned promise by having $promise object. That's why you were able to have .then(chain promise) function over it.
So basically you just need to push returned itemPromise object instead of having itemPromise.$promise inside promises array. Basically when you are doing $promise, it creates an array of [undefined, undefined, ...] and will resolve as soon as for loop completed.
Change to
promises.push(itemPromise)
from
promises.push(itemPromise.$promise)
Somewhat this question can relate to this answer
I am executing async AJAX requests which are being wrapped into function. Where $.ajax is Deferred object and I can use .promise properly (check: Initially Loaded) then I won't be able to do the same with 'Now really loaded' which will be executed before ajax finish loading.
function WSCall(method, data, callback, type, async, bg) {
// .. code ..
var promise = $.ajax({
'url': useSampleData ? useSampleData || null,
//'async': false,
'type': 'POST',
'dataType': (type == null) ? 'json' : type,
'data': data,
'beforeSend': bg ? null : LoadingBegin,
'complete': bg ? null : LoadingEnd,
'success': callback,
'error' : bg ? null : function(jqXHR, textStatus, errorThrown) { networkError = 1; }
});
promise.done(function(){ console.log('Initially loaded') });
}
function aSyncEvent() {
WSCall(
'status',
{},
function (data) {
if (data.error) {
console.log('Error occured'); return ShowDialogAlert(data.error); }
if (data.statusResult) {
var parts = data.statusResult.split('-');
if (parts[1] === '0') {
sId = parts[0];
console.log('Wow its loaded!');
return true;
}
}
}
)
}
$.when( aSyncEvent() ).then( function () { console.log('now really loaded')});
Initially loaded and Wow its loaded will appear properly AFTER ajax has been executed in proper order however 'now really loaded' will appear before ajax finishes executing.
I beg for help regarding this matter.
Thanks
Mike
Have you tried returning your deferred?
function WSCall(method, data, callback, type, async, bg) {
// .. code ..
return promise.done(function(){ console.log('Initially loaded') });
}
and
function aSyncEvent() {
return WSCall(
// .. code ..
);
}