How to use deferred objects to wait until AJAX is done - javascript

Workflow:
With the macro goal of not having the user wait for anything, this is the user-flow of what I'm trying to do:
User fills out form
User clicks button to submit form
Form submits async via AJAX (FWIW, processing takes ~5 seconds)
Immediately after the form is async submitted, jQuery pops up a survey modal
User can either fill out survey and click button to submit it, or click button to skip it entirely
What I'm having trouble accomplishing now is the effect after user clicks submit-survey or skip-survey. It's easiest to talk about skipping first. If the user chooses to skip the survey, then this happens:
A "working" overlay appears over the page for as long as the async needs to still finish (if at all needed), because once the async finishes, it returns the right next action that needs to happen in the jQuery code (for example purposes, let's say this is a simple console.log)
If the user fills out the survey and clicks submit-survey, then what happens is this:
Survey submits async via AJAX
A "working" overlay appears over the page for as long as the async needs to still finish (exactly same as if skip-survey were clicked)
The additional complexity is that, the survey submits to a different route depending on if the first form is completed by the time the user clicks the submit-survey
Code
I'm taking this opportunity to learn more about async flows in general, which is why the code is a mess. Right now the implementation I'm thinking is that the form submission AJAX, once it's succeeded or failed, generates the 1) appropriate URL for survey submission and the 2) next action that submit or survey will ultimately trigger)
So something like...
$.ajax({
type : "POST",
url : '/orders/create_or_update',
dataType: 'json',
contentType: 'application/json',
data : JSON.stringify(params)
})
.done(function(){
// submit-survey post route
submit-url = '/orders/success_ajax_update'
// next action triggered by skip-survey and submit-survey
function nextAction() { console.log("fail") }
})
.fail(function(){
// submit-survey post route
submit-url = '/almost_orders/fail_ajax_update'
// next action triggered by skip-survey and submit-survey
function nextAction() { console.log("fail") }
})
$(document).on("click", "#submit-survey", function() {
if (isFunction(nextAction)) {
$.ajax({url: submit-url, ... })
nextAction()
} else {
addWorkingOverlay();
}
})
$(document).on("click", "#skip-survey", function() {
if (isFunction(nextAction)) {
nextAction()
} else {
addWorkingOverlay();
}
})
The problem with the above though, is if user clicks either submit-survey or skip-survey before the AJAX has finished, then the working overlay gets added, but nothing then says "oh now AJAX has finished, nextAction is defined, so loop again". And obviously I can add a counter to check back on isFunction(nextAction) but this seems to completely defeat the purpose of AJAX in the first place.
Ideas on how to fix this?
Have new idea, need more help to flesh out. Per documentation, you can use jQuery's .when like so:
$.when( $.ajax( "/page1.php" ), $.ajax( "/page2.php" ) )
.then( myFunc, myFailure );
This can work in my case, only instead of a second .ajax I have a button click (either submit or skip). The question with this idea becomes, how do I turn the user's click of either submit or skip button into a resolved deferred that the .when can work with?

Related

Wait for form.submit() / POST to complete

I'm stuck in a really bizarre situation here. It's complicated to explain but I'll try my best.
Detailed explanation of the issue:
On every top Nav click (Green donuts/circles), or next button, I must submit the form, if it exists and is valid. If not valid, form.valid() triggers validation errors and return false would stop any further propagation. This setup was working flawlessly until I noticed a strange behavior which isn't very persistence. Form on my 3rd tab, specifically, is quite data heavy. When I hit next button it should practically go thru the same process: check for an existing form, if valid, then submit. Submit calls the POST action method and when post completes it GETs the view for next tab. It works like this 5/10 times but at other times GET executes before the POST, which causes next page to load with incomplete data. When I put breakpoints to debug, I see GET for the next tab executing before POST of the current tab.
UI Explained:
I have a UI with 4 navigation <a> buttons on top - in the center there's a always a form - and at the bottom I have Previous & Next buttons.
Forms are constructed in MVC using Ajax.BeginForm
For each Nav link <a> element on top, I have a JavaScript function
var LoadTabs = function (e, arg) {
// This is to validate a form if one of the top links is clicked and form has incomplete fields...
if (arg !== "prev" && arg !== "next") {
if (!window.ValidateForm(false)) return false;
}
var url = $(this).attr('data'); // this contains link to a GET action method
if (typeof url != "undefined") {
$.ajax(url, { context: { param: arg } }).done(function (data) {
$('#partialViewContainer').html(data);
});
}
}
This function above binds to each top link on page load.
$('.navLinks').on('click', LoadTabs);
My Next & Previous buttons basically trigger the click event i.e. LoadTabs function.
$('button').on('click', function () {
if (this.id === "btnMoveToNextTab") {
if (!window.ValidateForm(true)) return false;
$.ajax({
url: url,
context: { param: 'next' },
method: "GET",
data: data,
success: function(response) {
if (typeof response == 'object') {
if (response.moveAhead) {
MoveNext();
}
} else {
$('#mainView').html(response);
}
ScrollUp(0);
}
});
}
if (this.id === "btnMoveToPreviousTab") {
MoveBack();
}
return false;
});
MoveNext() Implementation is as below:
function MoveNext() {
var listItem = $('#progressbarInd > .active').next('li');
listItem.find('.navLink').trigger('click', ['next']);
ScrollUp(0);
}
The problem is, for some reasons, when Nav Link 3 is active and I hit NEXT button - Instead of posting the form first via form.submit() - the nav 4 gets triggered - hence GET for nav 4 runs before form POST of nav 3.
My ValidateForm method is basically just checking if the form exists and is valid then Submit, else returns false. Its as below:
function ValidateForm(submit) {
var form = $('form');
// if form doesn't exist on the page - return true and continue
if (typeof form[0] === "undefined") return true;
// now check for any validation errors
if (submit) {
if (!$(form).valid()) {
return false;
} else {
$(form).submit();
}
}
else {
return true;
}
return true;
}
My speculation is that form.submit does get triggered as it should be but since submit takes a little longer to finish it continues with the next code block in the button onclick event.
I first thought that this is a server side issue as in the POST I'm saving a big chunk of data with a few loops, and any code block that's process heavy I have that part in
var saveTask = Task.Factory.StartNew(() => ControllerHelper.SomeMethod(db, model)); Task.WaitAll(saveTask);
WaitAll will wait and pause the execution until SomeMethod finishes executing. I'm not sure how can I lock a process in JavaScript and wait for it to finish execution. Because I think If i can somehow lock the form.submit() in ValidateForm until its finished processing .. via a callback method perhaps...
Please if anyone can put me in right direction, I'd greatly appreciate the help. If you need more information please let me know I'd be happy to provide!
Ajax is async, and your forms submit which is using Ajax.BeginForm() is using ajax. What is happening is that when you click your 'Next' button, which triggers the $('button').on('click', function () { code:
You call the ValidateForm() function (and assuming its valid),
your $(form).submit(); line of code starts making a ajax POST
The code progresses to the final return true; line while the ajax
call is executing.
Because the ValidateForm() function returned true, the $.ajax
GET call now starts, but at that point the ajax POST in the
ValidateForm() function may not have finished executing causing
your GET method to return invalid data
You need to change your code so that the GET call is made once the POST method call has completed. And since your using the $.ajax() methods throughout your code, and $.ajax() gives you more flexibility, it seems unnecessary to use Ajax.BeginForm() (and the extra overhead of including the jquery.unbtrusive-ajax.js script). You should also be handling the forms .submit() function (if you do not want the 'Next' button to be a submit button in the form, you could just trigger the .submit() event in the buttons .click() handler)
$(document).on('submit', 'form', function(e) {
e.preventDefault(); // cancel default submit
var form = $(this);
if (!form.valid()) {
return; // will display the validation errors
}
.... // get the relevant urls to the GET and POST methods etc
$.post(postUrl, form.serialize(), function(data) {
.... // not clear if your [HttpPost] method returns anything
}).done(function() {
$.get(getUrl, someData, function(response) {
.... // Update the DOM with the next form?
.... // Re-parse the validator for client side validation
}
}).fail(function() {
.... // code that you might run if the code in the [HttpPost] method fails
});
});
You should also consider returning the appropriate 'next' view in the [HttpPost] method so that you don't then needs to make a second call back to the server to get it.
It is also worth reading the Deferred Object documentation and the use of $.when(), $.then() etc.

query ajax- change return value of calling function based on AJAX response?

This question has been asked and answered many times with the answer "call another function from the success callback" Unfortunately, I can't figure out how to make that answer work in my situation, for a couple of reasons.
For starters, the ajax being called is the submit function of an ajaxForm.
In one place I call the ajax submit, the calling function ITSELF needs to return either true or false, depending on the success of the AJAX call and some data returned from it. This is based on a tab widget used in multiple places: you can bind a function to the tab, which is run when the tab clicked. The tab widget itself expects this function to return either true (show the tab) or false (don't show the tab). Since this tab widget is used throughout my site, changing this behavior would be difficult at best.
The form itself can be submitted from more than one place, so I can't simply call the rest of this tab function from the success handler (even if I re-wrote the tab handler to work asynchronously), as that may not be the appropriate response to success when the form is submitted elsewhere. In this case, I want to show the tab the user clicked. In another case I want to reload the page or reset the form (new, blank form), and in a third case I want to go to a different page. But in all cases, it is in response to submitting the same form.
I suppose I could make it so the tab ALWAYS switches when clicked, and then (by setting flags or something so this only happens after a tab click) if the submit doesn't work, switch back, but that feels quite kludgy. So, is there any way to make the calling function wait until the callbacks are complete? Or is there another way to structure things that I am not thinking of?
EDIT:
I now have a jsfiddle of the current structure available here. The general idea is that back before I knew jquery had a tab widget, I needed tabs, so I wrote my own. It works, so I've seen no reason to change :-).
On document ready, each button with the appropriate class gets a click handler assigned. The flow is as follows:
The onClick handler triggers a beforeShow event to run the function (if any) bound to the tab object
The beforeShow function does any sort of prep work (such as loading data) desired before showing the tab. If needed, the 'show' data of the tab object can be set to false here to prevent the tab from being shown - such as if an error was detected on the current tab that needs to be fixed before switching.
After the beforeShow function returns, the onClick handler checks to see if the 'show' data was set to false, and if not hides all other tabs and shows the clicked tab.
So my dilemma here is that to prevent the tab from being shown, I need to set the 'show' data of the tab object to false before the onClick handler checks for it (i.e. during the beforeShow function). But with the AJAX callbacks being asynchronous, I don't get the result of the AJAX to see if I should show the tab or not until after the onClick handler has completed - thereby showing the tab.
As I mentioned, I can do something like store the current tab, and if the ajax results indicate NOT to show the new tab, switch back, but that just feels wrong to me.
The click handler:
function changeTab(){
var jqButton=$(this);
jqButton.data('show',true); //start by assuming we will show the tab.
if(jqButton.hasClass("current")) //don't do anything if switching to the current tab
return;
//run the "before show" function, if any.
jqButton.trigger('beforeShow');
var showTab=jqButton.data('show'); //see if the show flag has been set to false
if(showTab===false) //if no before show function, then showTab is null, not false
return; //if false, then don't show the tab.
$(".tabDiv").hide();
//run any "after hide" scripts bound to the current tab
$('.tabButton.current').trigger('afterHide');
var requestedDiv=jqButton.val(); //the "value" of the tab button is the ID of the tab content div
$("#"+requestedDiv).show(); //may show nothing, if no div exists for the current tab.
$(".tabButton").removeClass("current"); //remove the current class from all tab buttons, so it will only be applied to the new current tab button.
jqButton.addClass("current");
}
This is pretty much what you want
function tab2BeforeShow(){
var $form = $('#SomeajaxFormObject'); // This is the form you want to submit
// I'd probably show some load indicator here
var result;
$.ajax({
url: $form.attr('action'),
type: 'POST',
data: $form.serialize(),
async: false,
success: function(response) {
result = true; // set the result variable declared in outer function
},
error: function() {
result = false; // bad response - dont show new tab
}
});
// Hide your load indicator now
// since we declared async: false on the ajax request,
// we can be positive that result is set to a boolean value at this point
$(this).data('show', result);
}
We make a synchronous post to your forms action url, serializing the form's data so we can make an ajax POST with it. This should drop in, with maybe some minor tweaking.
The key factors here are that we set data to $form.serialize() and we set async to false, so the browser does not continue execution while the request is working.
If there is any way to avoid using synchronous ajax, you really should. If you can't avoid it, this should work for you. Do note that if the server hangs, it is going to hang your client as well.
This is a method to do it asynchronously.
Let the beforeShow handler accept an optional promise parameter:
var eventParams = {
promise: null
};
jqButton.trigger('beforeShow', eventParams);
In the handler itself, create a promise if necessary:
function tab2BeforeShow(e, eventParams) {
eventParams.promise = $.Deferred();
$.ajax(... async code...
var showTab = [some value received from request];
eventParams.promise.resolve(showTab);
);
}
Then if a promise is created, use a promise chain instead of synchronous function calls:
// doShowTab is the logic that actually shows/hides a tab
if (eventParams.promise) {
// use promise instead
eventParams.promise.then(doShowTab);
} else {
// beforeShow handler didn't return a promise, assume showTab=true
doShowTab(true);
}
Fiddle: http://jsfiddle.net/669v1Lcd/ (I used setTimeout to simulate an async request)

Why does `$.post` works only if `alert()` presents after it?

I have the following javascript code:
$.post("<receiver>", postdata);
And gets postdata not always. If I write the following code all works good:
$.post("<receiver>", postdata);
alert('bla-bla-bla, read me for a second');
Why? The page is changing on the save button as the javascript runs. But I need to send post data before redirecting.
You should redirect inside the success callback of your AJAX call:
$.post("<receiver>", postdata, function() {
window.location.href = '...';
});
The reason why your code works if you put an alert immediately after the $.post call is because when this alert pops up, the browser suspends the execution and your AJAX call has enough time to complete.
Don't forget that the first A in AJAX stands for Asynchronous meaning that you could only consume the results returned from the server inside the success callback.
Also if this AJAX call is performed inside some .submit() event handler of a form or inside some .onclick() handler of a submit button or an anchor you should make sure that you have canceled the default action by returning false otherwise your AJAX call will never have the time to execute before the browser redirects away from the page.
Example:
$('#myForm').submit({
$.post("<receiver>", postdata, function() {
...
});
return false; // <!-- That's the important bit
});
Ah, so it seems that the missing portion of your question is you are sending data on click of something yes? Presumably a link? That link causes the browser to follow it immediately, and in your example the alert is delaying the browser enough that your post has enough time to complete.
You need to ensure that the default action of that link is blocked, and do the redirect in the callback of your $.post() instead:
$("a.some_class").click(function(evt)
{
evt.preventDefault(); // makes sure browser doesn't follow the link
// gather your post data here ...
var $this = this;
$.post("<receiver>", postdata, function()
{
window.location.href = $this.attr("href");
});
})
Your alert is causing your script to pause and therefore allowing time for your $.post() to complete.
You should put your redirect script in your $.post() callback.
because it causes a delay. While you press OK the request (which takes at least a few milliseconds) gets finished and the stuff depending on it can follow.
To prevent this, you can pass a callback function that runs after the request got its response.
$.post( url, postdata, function() {
// Success.
} )
The .post is asynchronous.
If you change page during the post process () the POST request will get aborted.
Your alert is preventing this page change
You should replace your .post with a .ajax synchronous request, validating form submission on success ( return true; ) . Or do as suggested by #DarinDimitrov or #Curt

Forcing post request to finish submitting before new page loads

I am trying to use the jQuery $.post method to submit an ajax request to a PHP script whenever a certain element is clicked. I don't care about the value returned by the server - I just want to make sure that my data is submitted.
However, the element that receives the click could contain either a hyperlinked image or a flash element with several links in it. How can I ensure that my script receives the post request (again, I don't care about its response) even if the user navigates away from the page? I am using the code below:
jQuery(document).ready(function($) {
$(".click-track").mousedown(function(e) { // click() will not pass through a flash movie, so we must use mousedown
ad_id = $(this).data("ad-id");
var data = {
action: 'log_click',
adId: ad_id
};
$.ajaxSetup({async: false});
$.post(myscript.php, data, function(data) {
// Do nothing because we don't care about the response
}, 'html');
});
});
I'm no browser scripting guru, and this code has me flummoxed. I would appreciate any help you could give!
If you do an e.preventDefault() within the mousedown handler, you can simply do this inside the $.post call:
$(".click-track").mousedown(function(e) { // click() will not pass through a flash movie, so we must use mousedown
e.preventDefault();
ad_id = $(this).data("ad-id");
var data = {
action: 'log_click',
adId: ad_id
};
$.ajaxSetup({async: false});
var jqxhr = $.post(myscript.php, data, function(data) {
// Do nothing because we don't care about the response
}, 'html');
jqxhr.complete(function(){ $(this).trigger('click'); });
});
This should create an ajax object and attach a function when the POST request completes that should trigger a click on the originally mousedown'd element. I've gotten this to work with non-Flash elements, not sure if it will work on the Flash object, but it should.
Handle the redirect in Javascript, and make it happen after you get a response. Also, you probably don't want AJAX -- you probably want a synchronous post.
If you want to force you code to block until the post finishes, I suggest using .ajax() instead and set the async field to false. Like so:
$.ajax({
//some fields
async:false,
//some more fields
});
Now, your js will block until the call returns. Though I will warn you this is not preferred.
Edit: If the element is a hyperlink you will need to call event.preventDefault(). It is also worthwhile to set the link's href attribute to "javascript:void(0);".

Send information about clicked link to the server before redirect

We're creating a click tracking app, that builds heatmaps. I'm writing a script which users are suppose to insert into their pages for tracking to work.
It works fine on elements, which doesn't require a redirect or form submit. For example, if I click on h1 or p or whatever, it works perfectly correct. But, if I click on a a, request to our server never happens before the normal redirect.
In the last couple of days I tried a lot of ways to do that. First of, I tried a normal AJAX call, since it was a cross-domain request I had to use JSONP, but again, that AJAX call did not have time to execute before the redirect. Adding async: false would have solved the problem, but it doesn't work with JSONP requests. So I decided to add a flag variable which indicates that it is safe to move on with redirect and used an empty while loop to wait until it becomes try in the ajax callback. But the while loop was blocking the execution flow, so callback never got a chance to set that variable to true. Here is some simplified code:
$(document).on('click', function (e) {
//part of the code is omitted
$.ajax({
url: baseUrl,
data: data,
type: "get",
dataType: "jsonp",
crossDomain: true,
complete: function (xhr, status,) {
itsSafeToMoveOn = true;
}
});
while(!itsSafeToMoveOn){}
return true;
});
The next thing I tried is to use unload page event to wait until total ajax calls in progress would become zero (I had a counter implemented) and then to move on with redirect. It worked in Firefox and IE, but in WebKit there was this error:
Error: Too much time spent in unload handler
After that I realized that I don't care about the server response and using img.src for the request would be an ideal fit for this case. So at this point code looks like this:
$(document).click(function (e) {
//part of the code is ommited
(new Image).src = baseUrl + '?' + data;
if (tag === "a" || clickedElement.parents().has("a")) {
sleep(100);
}
return true;
});
That way I increased the overall script performance slightly, but problem with links remains unchanged. The sleep function appears to be also blocking the execution flow and request never happens.
The only idea left is to return false from the event handler and than redirect manually to the clicked element's href or to call submit() on the form, but it will complicate things to much and believe me it's already a huge pain in the ass to debug this script in different browsers.
Does anyone have any other ideas?
var globalStopper = true;
$(document).on('click', function (e) {
if (globalStopper === false)
return true; //proceed with click if stopper is NOT set
else {
globalStopper = false; //release the breaks
$.ajax({
//blahblah
complete: function (xhr, status,) {
$(elem).click(); //when ajax request done - "rerun" the click
}
});
return false; //DO NOT let browser process the click
}
});
Also, instead of adding image, try adding script. And then add the script to the HEAD section. This way the browser will "wait" until it's loaded.
$(document).on('click', function (e) {
var scriptTag = document.createElement("script");
scriptTag.setAttribute("type", "text/javascript");
scriptTag.setAttribute("src", url);
document.getElementsByTagName("head")[0].appendChild(scriptTag);
return true;
}
I would take a look at the navigator sendBeacon API mentioned in this stack overflow answer or directly linked to here.
From the description on the site
navigator.sendBeacon(url, data) - This method addresses the needs of analytics and diagnostics code that typically attempts to send data to a web server prior to the unloading of the document.
You can save information to ajax request in cookies or localStorage and make any worker that will send information. Saving to cookies or localStorage is faster then ajax-request. You can do next:
$(document).click(function (e) {
var queue = localStorage.getItem('requestQueue');
queue.push(data);
localStorage.setItem('requestQueue',queue);
});
$(function(){
setInterval(function(){
var queue = localStorage.getItem('requestQueue');
while (queue.length > 0) {
var data = queue.pop();
$.ajax({
...
success: function(){
localStorage.setItem('requestQueue', queue);
}
});
}
},intervalToSendData);
});
So, when user click on link or send a form, data will be saved to storage and after user go to next page, this worker starts and send data to your server.
The JavaScript is basically executed in single thread. It is not possible to have your callback function executed and at the same time have an infinite loop waiting for a flag variable from it. The infinite loop will occupy the single execution thread and the callback will never be called.
Best approach is to cancel the default handler of your event and bubbling for it (basically return false if you are really building your tracking code with jQuery), and do the necessary actions (redirect page to the necessary address if a link was clicked or trigger other default actions), but this would take a lot of careful work to recreate all the possible combinations of actiona and callbacks.
Another approach is to:
1) Look for something specific to your code in the event data
2) If it is not present - make an AJAX call and in its callback re-trigger the same even on the same element, but this time with your specific bit added to the even data; after the AJAX call return false
3) If your specific bits are present in the data - simply do nothing, allowing the default event processing to take place.
The either approach may bite, however.
So if I understand right, you want your ajax logs completed before the page unloads and follows a link href. This sounds like a perfect case where you could consider using Deferreds in jQuery.
When your user clicks on anything that's supposed to take him away from the page, just check your promise status. If it's not resolved, you could throw a modal window over the page, and ask the user to wait til the progress is complete. Then, add a new pipe to your deferred, telling it to change the location href once everything is complete.
Let me know if this is the scenario. If it is, I'll explain in more detail. No use continuing if I didn't understand your requirement properly

Categories