Gaurav Aggarwal Gaurav Aggarwal - 6 months ago 39
Javascript Question

Reload page after .each function completes

I am creating an array and pushing AJAX queries using jquery

$.ajax
with
$.each
function. I want that after the completion of array the page should refresh, but when I try to do this the page refreshes in between. When I use
async:false
for
$.ajax
queries, it works fine but this causes performance degradation (time taken for the ajax queries to complete increases by 5 times). Can someone give me pointers on how to go about it, following is the code I put in:

$('#update1').click(function()
{
$.each(testarray,function(k,valu)
{
var count = testarray.length;
var checkloop = k+1;
var valkor = "/webresources/api/v3/sites/current/pages/" + valu.value;
var valkis = valu.checkbox;

var request =
$.ajax(
{
url: valkor,
processData: false,
data: JSON.stringify({ "enabled" : valkis }),

type: "PUT",
contentType: "application/json",
mimeType: "application/json ",
headers: { "Authorization": $.cookie('access_token') }
})

request.done(function(msg)
{
console.log("successfully updated");
})

request.fail(function(jqXHR)
{
console.log("Request failed.");
console.log("Error code: " + jqXHR.status);
console.log("Error text: " + jqXHR.statusText);
console.log("Response text: " + jqXHR.responseText);
})

request.then(function()
{
location.reload(true);
})
})
})


The above code refreshes the page without waiting for the loop to complete. I have even tried for loop. Please let me know what am I missing.

Answer

You can use jQuery .when to defer action until after all AJAX requests complete.

Something like this:

function updateHandler()
{
    var promisesArray = [];
    var successCounter = 0;
    var promise;

    $.each(testarray, function(k, valu)
    {
        var count = testarray.length;
        var checkloop = k+1;
        var valkor = "/webresources/api/v3/sites/current/pages/" + valu.value;
        var valkis = valu.checkbox;

        promise = 
            $.ajax(
            {
                url: valkor,
                processData: false,
                data: JSON.stringify({ "enabled" : valkis }),
                type: "PUT",    
                contentType: "application/json",
                mimeType: "application/json ",
                headers: { "Authorization": $.cookie('access_token') },
            });

        promise.done(function(msg)
        {
            console.log("successfully updated"); 
            successCounter++;
        });

        promise.fail(function(jqXHR) { console.log("XXX"); /* error out... */ });

      promisesArray.push(promise);
    });

    $.when.apply($, promisesArray).done(function()
    {
        location.reload(true);
        console.log("WAITED FOR " + successCounter + " OF " + testarray.length);
    });
}

$('#update1').click(updateHandler);

Refer to the docs for $.when.

Since jQuery 1.5, calls like $.ajax return a promise. These are useful for a number of different things (see this post for other useful things about promises), but in our case, we'd like to collect the promises to feed to a $.when call, in order to defer execution of a piece of code until all the AJAX requests return a response.

In the $.each loop, we define the $.ajax calls. As soon as each one is defined - after each call to $.ajax - a request is sent across the network and the browser will wait for the response. In addition we collect the associated promise for each individual $.ajax call in a variable. In the code above, we do two things with each of these these promises: 1) we append individual done and error functions for taking action after each call completes; 2) we store the promise in promisesArray so that we can take action only after all the calls have completed.

The most important thing to note here is that this line: promise = $.ajax(... doesn't store the result - i.e. the response - of the AJAX call, it stores a promise associated with the AJAX call.

Finally, we hand these promises to $.when, which uses it's own done function to take the action that we want to take only after all the AJAX calls have returned.

The solution above collects an array of promises, because my assumption from your question and your usage of testarray is that we don't know ahead of time how many AJAX calls will be necessary. If you did know how many AJAX calls you would use, we can remove promisesArray from the solution, which could look something like this in the case of 3 AJAX calls:

$.when(
    $.ajax( /* args for 1st call */ ).done( /* 1st call success code */ ),
    $,ajax( /* args for 2nd call */ ).done( /* 2nd call success code */ ),
    $.ajax( /* args for 3rd call */ ).done( /* 3rd call success code */ )
    ).done(function()
      {
          location.reload(true);
      });

Since we may not know how many calls are necessary, e.g. due to user input, then we need to collect the promises associated with the unknown number of $.ajax calls.

Before jQuery supported deferred execution - prior to jQuery 1.5 - another way to deal with the problem of take an action only after these other N (typically AJAX) actions complete was to keep a big state variable: with N actions to complete, you produce an array of N booleans. When each action completed (an AJAX call returned), the done handler would do two things: 1) set it's flag to true; 2) check to see if all the flags have been set to true, and then take the action, for instance, re-loading the page. This worked because of the single-threaded nature of JavaScript event queue handling.

There are a few more things to note here:

  • You need the apply because $.when expects the deferreds as separate parameters.
  • I've added a counter that's incremented on each of the AJAX done calls, if you comment out the location.reload call you should see - assuming that the PUT calls are successful - that the value of the counter matches the size of testarray.
  • you should consider the difference between using $.when().done vs. $.when().always() to determine which suits your own code better.
  • I've pulled the setup of the $.each and $.when loops into a separate function. This is inconsequential, and you can leave them in the click as in your original code and it will still work.
  • I've tested this by adding a button with update1 as the id and hitting https://httpbin/put as your valkor URL.

Generally don't use async: false; - when you find yourself doing this always back up and look for another way. jQuery/JavaScript always provides one.