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.
Related
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?
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)
I have a lot of buttons with the class search_camera_btn.
When clicking on the button then it submits a form. This step works. On the other side, it should also trigger a button click event.
I wrote the event listener in a coffeescript file which sends Ajax requests, but it only works on the first click.
I put the code in this gist.
The Javascript works when the button is clicked the first time, but fails on subsequent clicks.
Actually I put a alert message in the begin of click event handler,
But it only alerts at the first time.
And there is no error message in my Firbug console. (I thought it is just didn't fire the click event handler.)
$(".search_camera_btn").click ->
alert "test"
There are many buttons,no matter which button I click.
It always works at the first time click.
Here is my more detail source code. download
Any ideas?
I narrow down the buggy code.That is the "Ready to enter start" message only called at the first time.
But there is no error showed on the Firebug Javascript console and Rails console.
Should I enable some settings in the development mode ?
IW2 = get_camera_list: ->
console.log("Start")
ajax_req = $.ajax
url: "update_camera_list/"
type: "GET"
success: (resp) ->
# console.log resp
res = setTimeout (->
ajax_req
), 500
console.log("End")
return
jQuery ->
$(".search_camera_btn").click ->
console.log("Ready to enter start")
IW2.get_camera_list()
Compiled CoffeeScript:
var IW2;
IW2 = {
get_camera_list: function() {
var ajax_req, res;
console.log("Start");
ajax_req = $.ajax({
url: "update_camera_list/",
type: "GET",
success: function(resp) {}
});
res = setTimeout((function() {
return ajax_req;
}), 500);
console.log("End");
}
};
jQuery(function() {
return $(".search_camera_btn").click(function() {
console.log("Ready to enter start");
return IW2.get_camera_list();
});
});
The reason that the handler is only being fired once is because
the ".click" handler only applies to elements that are currently attached
to the DOM. If you replace the HTML after making the AJAX call, the event handlers will be lost.
You should use an event delegate instead. Try this:
jQuery(function() {
return $("body").on("click", ".search_camera_btn", function() {
alert("Ready to enter start");
return IW2.get_camera_list();
});
});
This statement basically says, if there are any elements in the DOM now, or in the future, that have a class of "search_camera_btn", and are contained within the "body" element, then fire the click handler.
I hope this helps!
Change this:
jQuery ->
$(".search_camera_btn").click ->
console.log("Ready to enter start")
IW2.get_camera_list()
For:
jQuery ->
$(".search_camera_btn").click ->
console.log("Ready to enter start")
IW2.get_camera_list()
return
And let me know if it helps ;)
I would make sure nothing else in your application's javascript is failing (like a simple syntax error can cause this sort of thing). Also have you tried this in different browsers? I've had a similar problem where my Ajax would work fine in Chrome but would only post once in Firefox because of some add-ons/extensions (and then I disabled them and they worked).
Also, I'm not sure if I read your gist correctly but it looks like you're specifying .click in both the jQuery of the application.js and in the btn.js.coffee, and I'm pretty sure that section in application.js should just be watching for the element's function/result and not specifying the click again.
If nothing else works, also check out ajax's .done completion call about halfway down the page here: http://api.jquery.com/jQuery.ajax/ . Then show the picture list as a function after .done so you know your ajax post is always completing before moving on to the next thing. (Ajax problems like this often tend to be server side when a call doesn't complete or there's a loop somewhere)
I am using bootstrap and JQuery. I want to know if its possible to invoke a bootstrap modal dialog before making an ajax call in '$.ajax beforeSend'? I want to collect user comments before submitting the form. I have several buttons on my page that require this behavior. So, I want to make it more generic.
Thanks
I'd suggest the use of jQuery's Deferred object (see http://api.jquery.com/category/deferred-object/).
The following is pseudo code for your buttons' event handler:
$('#theButton').on('click', function() {
var dfd = $.Deferred();
// Code to show modal dialog here.
// Pass dfd to the dialog, and have it call dfd.resolve()
// when the user has finished, or dfd.reject() in case the
// user does not complete the form.
dfd.done(function() {
// Ajax call here
});
});
The function that gets passed to dfd.done() as an argument gets only called if and when somebody calls the resolve() method on the Deferred object.
Due to the asynchronous event model in javascript, you cannot postpone the ajax request from being sent off from within beforeSend. Once beforeSend is executed, the only chance you have to "delay" the ajax request is to outright cancel it entirely by returning false from the callback.
So while you could keep track of a status variable that knows whether the form is ready to submit (return false from beforeSend whenever the form is not ready), you're much better off doing these validation checks before ever creating the ajax request to begin with.
// why do this
$.ajax('/path', {
beforeSend: function () {
if (formIsNotReady()) {
showModal();
return false;
}
}
});
// when you can do this
if (formIsNotReady()) {
showModal();
} else {
$.ajax('/path');
}
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