10 Feb 2008

Nearly a year ago, I rebuilt a business website. On this website, there is a “Contest” page that offers each month a gift to the randomly selected winner.

However, this contest is supposed to be available only for Belgian.

First Solution

A few years ago, when I first built the website and that we encountered this problem (having French, German and event Lebanese people taking the contest), we thought of a simple solution: when someone fills in his post code in the participation form, a simple JavaScript code (using AJAX) would then populate a Combo box with all the cities that matched the given 4 digit Belgian post code.

After making up a post code table in the database, I wrote up a php page that would return a list of cities associated with the given post code.

Here is an example of the JSON response from that page, when called with 5300 as post code:

{ cities : ['Andenne','Bonneville','Coutisse','Landenne','Maizeret','Namêche','Sclayn','Seilles','Thon','Vezin'] }

I then wrote my first AJAX script (early 2006), that would call this page with a XMLHTTPRequest. As soon as there were 4 digits in the postal code fields, the request was launched. Once the response received, the combobox was filled with options made using the received list.

Using this technique, we enforced participants to enter a valid Belgian post code. Actually, that didn't enforce anything as participant could leave blank fields, but we could easily sort relevant contest participants in the database.

Accessibility Issue

Last year, I rebuild the website from ground and realized that there was an accessibility issue. What would happen to the poor people which don't run JavaScript? They would all be rejected even is they were Belgian!

I had to find a way to make sure that people that didn't have JavaScript would still be able to fill the form in.

Second Solution

I thought of a simple way to avoid that. On the initial page, I replaced the select tag (Combo box) by an input tag (single line text input).

I then coded a event that would replace that input tag by a select tag.

I used the excellent MooTools JavaScript framework for this code.

Here is the Javascript code:

window.addEvent('domready', function() {
    //Replaces the input field by a select field
    oldEdtCity = $('edtCity');
    newEdtCity = $(document.createElement('select'));
    newEdtCity.setAttribute('id', 'edtCity');
    newEdtCity.setAttribute('name', 'city');
    newEdtCity.options = new Option('- Enter your postal code -', '- Enter your postal code -');
    oldEdtCity.parentNode.replaceChild(newEdtCity, oldEdtCity);
    //Adding event watcher on the postcode field
    $('edtPostcode').onkeyup = function(e) {
        var target = new Event(e).target;
        if (target.getValue().length  4) {
            new Ajax('_ajax.php?', Object.toQueryString({'task': 'postcode', 'cp': $('edtPostcode').getValue()}), {
                method: 'get',
                onComplete: function(responseText) {
                    //Function that will populate the select with cities returned by the ajax request
                    var response = Json.evaluate(responseText);
                    var cities = $('edtCity').options;
                    cities.length = 0;
                    if (response.cities ==  '') {
                        cities = new Option('- No corresponding city -', '- No corresponding city -');
                    }
                    else {
                        cities = new Option('- Choose -', '- Choose -');
                        response.cities.each(function(city) {
                            cities[cities.length] = new Option(city, city);
                        });
                    }
                }
            }).request();
        }
    };
});

So, when the page loads, it contains a simple input tag, and once it has loaded, the Javascript replaces that input tag by a select tag.

Error Handling and Timeout

This is far from being perfect. What if the AJAX request fails or timeouts?

Even if the the errors are different, the resolution is the same for both: simply turn back the select tag into a input tag. So in this case, one function will be called, no matter which error rises:

function requestFailure() {
    //Replaces the select field by an input field
    oldEdtCity = $('edtCity');
    newEdtCity = $(document.createElement('input'));
    newEdtCity.setAttribute('id', 'edtCity');
    newEdtCity.setAttribute('name', 'city');
    newEdtCity.setAttribute('type', 'text');
    oldEdtCity.parentNode.replaceChild(newEdtCity, oldEdtCity);
}

Error Handling

As is, the MooTools framework didn't support error handling in Ajax object. However, one of this framework developer, Aaron Newton, wrote an Ajax extension that is to be used with the framework in order to specify a function that will be called if the request fails.

As said before, when I wrote that code the MooTools framework didn't support error and timeout handling. As of now, the latest version of MooTools supports both error and timeout handling. I think I will soon rewrite a new version of this code using the latest MooTools version, as it is more elegant.

Once this extension added, we simply need to give a onFailure parameter to Ajax object call in the previous code.

Here is what the new Ajax object (abbreviated) call looks like :

new Ajax('_ajax.php?' + Object.toQueryString({'task': 'postcode', 'cp': $('edtPostcode').getValue()}), {
    method: 'get',
    onComplete: function(responseText) { [...] },
    onFailure: requestFailure }
).request();

That's it. If the request fails, requestFailure function is called.

Timeout Handling

Again, nothing for timeout handling in MooTools at the time I wrote that code.

I used Jeremy Keith excellent technique described in his book Bulletproof Ajax.

The technique is simple: after starting the request, simply launch a timer that cancels the request after a certain time. So when the requests takes too long, it is canceled and the requestFailure function is called:

var timer = setTimeout(function() {
    ajx.cancel();
    requestFailure();
}, 5000);

If the request success, the timer must be stopped:

clearTimeout(timer);

So, adding all this code to the original function gives us the final code:

window.addEvent('domready', function() {
    [...]
    $('edtPostcode').onkeyup = function(e) {
        var target = new Event(e).target;
        var ajx, timer;
        if (target.getValue().length == 4) {
            ajx = new Ajax('_ajax.php?' + Object.toQueryString({'task': 'postcode', 'cp': $('edtPostcode').getValue()}), {
                method: 'get',
                onComplete: function(responseText) {
                [..]
                },
                onFailure: requestFailure
            });
            timer = setTimeout(function() {
                ajx.cancel();
                requestFailure();
            }, 5000);
            ajx.request();
        }
    };
});

I used a 5 seconds timers as this has to be fast. The user will not wait for a long time, especially as the select field directly follows the input field that launches the request.

Conclusion

The main point here was to show how to build a form that uses Ajax while making sure that users that don't use JavaScript will still be able to fill data in.

So, what do you think? Is it usefull for you?



blog comments powered by Disqus