It’s All In The Timing

I have been delving into AJAX more and more over the past twelve months or so and I came across a prickly problem recently that had me stumped for a while. AJAX is a great way of making web sites more dynamic and interesting and usable but the fact is that JavaScript is not the easiest scripting language to debug even if you do use great tools like Firebug.

I was integrating a purchase form which requires the user to enter a billing address as well as a shipping address. Now obviously, for a lot of people, their shipping and billing addresses are the same so we provide a checkbox that they can check rather than entering the address a second time. What usually happens when the user checks this checkbox is that the shipping address fields are automatically filled in with values from the billing address and then the fields are disabled so that the user knows they don't have to fill them in. This is a fairly simple process and we have been doing it for as many years as I can remember, even before AJAX became popular.

However, there is always a catch. You see, the form included a country field which is a drop-down box and, once the user has selected their country, there is also a state field in a drop-down box which is automatically populated with the appropriate states/provinces for the country that the user has selected.

This used to be accomplished by generating tons of JavaScript arrays within the page for each possible country and state that the user could select and when the country was selected it was a simple matter to look up the appropriate array for the list of states. But, of course, using this method means that the size of the initial page can get extremely large and bloated with a lot of information that the user will never use.

In order to get the page size down it make a lot more sense to skip the loading of massive arrays and to just load the array that you need via AJAX once you know which country the user has selected. This is also a much simpler process than generating all the arrays in the first place. You simply create a script which returns the array of states in JSON format for a given country and then add the following code to your page:

  $('select#bill_country_id').change(function(){    $.getJSON('/scripts/select_state.php', {country_id: $(this).val()}, function(j){      var options = '';      for (var i = 0; i < j.length; i++) {        options += '' + j[i].optionDisplay + ''      } // end for      $('select#bill_state_id').html(options)    })  })

This process seems to work extremely well and the script to retrieve the states for Australia takes only 85ms on the development server. Of course, this functionality is duplicated for the shipping address as well as the billing address and this is where the problem come in. Because, when the user checks the checkbox to signify that the billing address and shipping address are the same, the list of states for the billing address and shipping address do not necessarily match.

The problem I was experiencing was that, when the shipping country was updated, the list of states was not being updated correctly, or, if it was, the correct state was not being selected. I spent quite some time looking into the problem and trying to work out what was going on before it finally hit me that it was all a matter of timing. The problem was that, when the shipping country changed, it tried to do an AJAX call to get the list of states. Meanwhile, without waiting for that look-up to finish, it had already gone ahead and set the values of all the fields, including the state, and disabled the fields.

Because the lookup hadn't completed yet, the correct state was not being selected. Even though it only takes 85ms to look up the states, this is a lot longer than the time it takes to copy the field values from the billing address to the shipping address and disable the fields.

Of course, there is a very simple solution in this particular situation because there is no need to perform the AJAX call at all. You already have the list of states that you need in the billing address and you can just copy it across as follows:

  $('select#ship_state_id').html($('select#bill_state_id').html())

But this won't necessarily work in all scenarios so what is the solution? The solution that I came up with is not to rely on an AJAX call being finished or not if you need to manipulate an object being updated by that AJAX call. Instead, what you need to do is to do any manipulation of the object in the callback function itself. This will ensure that the manipulation only occurs once the AJAX call has been completed and you can be sure of the state of the object. Following is an example of how the above problem could be solved in this way:

  $.getJSON('/scripts/select_state.php', {country_id: $("[name='bill_country_id']").val()}, function(j){    var options = '';    for (var i = 0; i < j.length; i++) {      options += '' + j[i].optionDisplay + ''    } // end for    $('select#ship_state_id').html(options)    // As this function may not complete before the instructions below are executed we should set the state here just in case    $('select#ship_state_id').val($('select#bill_state_id').val())  })

 
Of course, you can then do the updating of all the other fields in the usual way outside of the callback function:

  $("[name^='ship_']").each(function(idx, field) {    $("[name='" + field.name + "']").val($("[name='" + field.name.replace('ship_', 'bill_') + "']").val())  })  // Disable all shipping fields...  $("[name^='ship_']").attr('disabled', $('input#ship_same').attr('checked'))  // ...except itself  $('input#ship_same').attr('disabled', false)

About Jared

Jared has over 10 years experience developing applications for the web. He can often be found in a dimly light room with the music blasting hacking away at a project to the early hours of the morning.

Speak Your Mind

*