Django tips: A simple AJAX example, part 2

Published August 5, 2006. Filed under: Django, JavaScript, Programming.

Last time around we looked at how to write a simple view which processes a form and either returns errors or returns success, and then tweaked it slightly so that the same view could handle either a “regular” form submission (in which case it operates normally), or an XMLHttpRequest (in which case it returns JSON).

Today we’ll look at writing the JavaScript side of it; for reference, here’s the live example we’re going to build. This will be a fairly long write-up, but that’s not an indication of the complexity of the process; I’m deliberately being much more verbose than necessary in order to cover a few important points about good JavaScript style and conventions.

All of the JavaScript we’re going to write in this article goes into a single file which, along with the necessary files from the YUI toolkit, will be pulled in by the example.

Choice of toolkit

There are lots of good, free, open-source JavaScript toolkits available these days, and generally you should take a look at a few of them and figure out which one best suits your needs. For this example I’m working with YUI, which I’ve also been using a little bit at work. Your needs and preferences may vary, but there are a few reasons why I really like YUI:

The example we’re building will pull in things from four modules of YUI: the DOM library, the event library, the connection manager and the animation utility.

How AJAX programming works

Depending on your background, AJAX programming in JavaScript may feel a bit weird and unfamiliar, so before we dive in let’s look at a few of the key ideas. And keep in mind, if you’re relatively new to JS, that by far the best way to “get” it initially is to look at it as a programming language which happens to have an API for working with web pages, instead of as a feature of web pages which happens to feel like a programming language.

The first and most important thing to pick up is that the “A” in “AJAX” stands for asynchronous. Which means that unlike other types of programming, where we’d do something like

var response = open_url("http://example.com/")

And then go on merrily doing things with the response variable, now what we’re doing is saying something like, “open this URL, and I’m going to do other things while you wait for the response. When the response comes back, call this function over here”.

The other key thing to keep in mind is that we’re doing heavily event-driven programming. This means that, rather than writing the program and having it just run until it’s done, we’re going to be writing pieces of code and hooking them up to be run if certain events — user interactions like typing something or clicking a button — take place in the page.

At first this may feel strange to you, since it seems like you’re giving up control of your program flow (since it’s event-driven you don’t know when something will start executing, and since it’s asynchronous you don’t know when it will finish), but once you get into the mindset it’s not so hard.

Now, let’s dive in and write some code.

Good code structure is key

I mentioned above that one of the things I like about YUI is the fact that it adds only one object to the global namespace; all of its modules, classes, attributes and so on are defined as attributes of the global YAHOO object (so, for example, the events module is YAHOO.util.Event, the DOM module is YAHOO.util.Dom, etc.). Most of the really good JavaScript toolkits do this, with only a couple of exceptions (Prototype, I’m looking at you, you big awful namespace polluter). And when we’re writing our own JavaScript, it’s generally a good idea to follow suit. Since this is going to be an AJAX example, we’ll keep pretty much all of our code in an object called ajax_example. Start out by defining it:

var ajax_example = {};

Now, there are two ways to go about building up the object, and which one you use is your choice. I prefer to do as much as possible using pure JavaScript object literal syntax, like so:

var ajax_example = {
    attribute_one: 'foo',
    attribute_two: 'bar'
};

And that’s how I’ll be building this example. But it’s equally possible to do it like this:

var ajax_example = {};
ajax_example.attribute_one = 'foo';
ajax_example.attribute_two = 'bar';

It’s also conventional, when using an object to encapsulate your code like this, to use a function called init to handle initialization of any variables you’ll need or to structure the page’s environment to what your code will expect. In this case we need to a few things to “initialize” our code:

  1. Get references to the elements in the page which contain the form and which will display the results.
  2. Change the “results” element’s opacity so we can do a nice fading animation on it later.
  3. Hijack the form so we can submit it via AJAX instead of doing a “normal” submission.

So inside our ajax_example object, we add this code:

init: function() {
   // Grab the elements we'll need.                                                                                                                                      
   ajax_example.form = document.getElementById('ajax_example');
   ajax_example.results_div = document.getElementById('results');
          
   // This is so we can fade it in later.                                                                                                                                
   YAHOO.util.Dom.setStyle(ajax_example.results_div, 'opacity', 0);
   
   // Hijack the form.                                                                                                                                                   
       YAHOO.util.Event.addListener(ajax_example.form, 'submit', ajax_example.submit_func);
},

The comments in the code should give you an idea of what’s going on:

  1. We know the ids of the form and the “results” element, so we can use getElementById to grab them and assign them to attributes of the ajax_example object for later use.
  2. YAHOO.util.Dom.setStyle does pretty much exactly what it sounds like; it takes an element, a style property and a value, and sets that element to have that value for that style property. In this case, we’re giving the “results” div an opacity of 0 (since the fading animation we’ll do with it later will mean gradually changing its opacity to 1).
  3. YAHOO.util.Event.addListener also does what it sounds like; you feed it an element, an event to listen for, and a function to execute when that event happens. We use it here to “hijack” the form’s submit event.

But there are a few other things going on here which are worth noting:

We’re initialized; now what?

The next step is to write the function which will handle the form submission, which we’ve already indicated will be named submit_func. Here’s the code:

submit_func: function(e) {
   YAHOO.util.Event.preventDefault(e);
   
   // Remove any error messages being displayed.                                                                                                                         
   var form_dl = ajax_example.form.getElementsByTagName('dl')[0];
   var form_fields = form_dl.getElementsByTagName('dd');
   for(var i=0; i < form_fields.length; i++) {
      if(YAHOO.util.Dom.hasClass(form_fields[i], 'error')) {
         form_dl.removeChild(form_fields[i]);
      }
   }
   YAHOO.util.Connect.setForm(ajax_example.form);
   
   //Temporarily disable the form.                                                                                                                                       
   for(var i=0; i < ajax_example.form.elements.length; i++) {
      ajax_example.form.elements[i].disabled = true;
   }
   var cObj = YAHOO.util.Connect.asyncRequest('POST', '/examples/ajax/1/?xhr', ajax_example.ajax_callback);
},

Broadly speaking, there are four things happening in this function:

  1. First we call YAHOO.util.Event.preventDefault with the argument passed to our function. Since we were listening for an event (the form’s submit event), that event will be the argument. We don’t want the form to submit normally, and preventDefault lets us control that; it prevents the “default” action for the event (submission of the form) from happening. Note that the W3C DOM specification says that events should have a preventDefault method automatically, but browser implementation — in my experience, particularly in Safari — makes it tricky to do this reliably. YUI provides its own cross-browser preventDefault function to compensate.
  2. Then we cycle over the dd elements in the form to see if any have the class error (using YAHOO.util.Dom.hasClass, which again works around trickiness in trying to detect whether an element has a given class). If this is the first time the form has submitted there won’t be any errors, but it’s possible the user could be correcting errors from a previous submission. If we find any error messages (you’ll see in a bit that the function which displays them puts them into dd elements with class set to error), we remove them from page using the standard DOM method removeChild (which is why the code just above the loop grabs the definition list which contains all the form fields — removeChild has to be called from the parent element of the one to remove).
  3. We temporarily disable the form’s inputs, by looping over them and setting each one’s disabled property to true. We do this because AJAX programming is asynchronous; it’s conceivable that someone could sit there and keep clicking the submit button, or pressing Enter, and the form would submit multiple times. There’s no guarantee of what order the responses would come back in, and as a result we could end up overwriting a “correct” submission with error messages if the “correct” submission came back before the one with errors did. Temporarily disabling the form while a submission is “in the air” prevents this potential problem.
  4. We fire off the AJAX request. This is handled by first saying that we want to submit this particular form, which is what YAHOO.util.Connect.setForm handles (and note that it needs to be called before you disable the form), then using YAHOO.util.Connect.asyncRequest to send off the XMLHttpRequest. asyncRequest can take a number of optional arguments, but in this case only three matter: the HTTP method to use for the request (which will be POST), the URL to submit to and a callback object which can process the response; here it will be the as-yet-undefined ajax_example.ajax_callback.

Writing the response callback

Different JavaScript libraries offer different ways of specifying code to be executed when the response from an XMLHttpRequest is received, but mostly they all have you specify some sort of callback. With YUI, you actually specify an object to use as the callback, not a function. The attributes of the object should include at least two — named success and failure — which are functions, and YUI will see to it that the appropriate one gets called, depending on the response (most of the time, the success function will be called, but the failure function will be used if the XMLHttpRequest couldn’t connect, or if the server returned an error code).

In this example we know that the view works and shouldn’t be returning server errors, so we’ll concentrate on the success function, which looks like this:

success: function(o) {
   // This turns the JSON string into a JavaScript object.                                                                                                                                          
   var response_obj = eval('(' + o.responseText + ')');
   
   // Set up the animation on the results div.                                                                                                                                                      
   var result_fade_out = new YAHOO.util.Anim(ajax_example.results_div, {
                                                  opacity: { to: 0 }
                                               }, 0.25, YAHOO.util.Easing.easeOut);
   
   if(response_obj.errors) { // The form had errors.                                                                                                                                                
      result_fade_out.onComplete.subscribe(function() {
                                                ajax_example.results_div.innerHTML = '';
                                                ajax_example.display_errors(response_obj.errors);
                                             });
   } else if(response_obj.success) { // The form went through successfully.                                                                                                                         
      var success_message = document.createElement('p');
      success_message.innerHTML = 'Form submitted successfully! Submitted input:';
      var input_list = document.createElement('ul');
      var name_item = document.createElement('li');
      name_item.innerHTML = 'Name: ' + response_obj.name;
      input_list.appendChild(name_item);
      var total_item = document.createElement('li');
      total_item.innerHTML = 'Total: ' + response_obj.total;
      input_list.appendChild(total_item);
      YAHOO.util.Dom.setStyle(ajax_example.results_div, 'display', 'block');
      var result_fade_in = new YAHOO.util.Anim(ajax_example.results_div, {
                                                opacity: { to: 1 }
      }, 0.25, YAHOO.util.Easing.easeIn);
      result_fade_out.onComplete.subscribe(function() {
                                                ajax_example.results_div.innerHTML = '';
                                                ajax_example.results_div.appendChild(success_message);
                                                ajax_example.results_div.appendChild(input_list);
                                                result_fade_in.animate();
      });
   }
   result_fade_out.onComplete.subscribe(function() {
      //Re -enable the form.
      for(var i=0; i < ajax_example.form.elements.length; i++) {
         ajax_example.form.elements[i].disabled = false;
      }});
      result_fade_out.animate();
},

The first thing this does is set up a fade out of the “results” div; we’ll be using YUI’s built-in ability to “subscribe” functions to the completion of an animation in order to manage the timing of the various things we need to do.

If the JSON response has an attribute called errors, we use the function display_errors (which we’ll write in a moment) to show the error messages it contains, and subscribe that to the fade’s completion. We also clear out the innerHTML of the “results” div (to remove any success messages previously displayed).

If, on the other hand, the JSON response has an attribute called success, then we use a combination of DOM methods and innerHTML to build up the new contents of the “results” div, and build a “fade in” for the “results” div. We then subscribe the final DOM manipulation and the fade in to the completion of the fade out we set up earlier.

That’s a little complicated, but it makes sure that things happen in the right order:

  1. The “results” div will completely fade out.
  2. Then its contents will be cleared.
  3. Then the new contents will go into it.
  4. Then it will fade in again.

If we didn’t do this by subscribing to the fade-out’s completion, then that might not finish before the rest of the code executed, and our users might see the clearing and replacment of the contents partially take place before the fade-out was done.

Finally, we always subscribe one more function to the fade-out’s completion: a bit of code which re-enables all the form elements, so that the form’s contents can be edited and submitted again.

The last line of code in the success function just animates the fade-out of the “results” div, and when that’s done whatever code we set up to run after its completion will be executed.

Showing the errors in the form

There’s only one more significant piece of code to write, and that’s the show_errors function we’ll use to display the error messages returned by the view. It looks like this:

display_errors: function(error_obj) {
   for(var err in error_obj) {
      var field_container = document.getElementById(err + '_container');
      var error_dd = document.createElement('dd');
      YAHOO.util.Dom.addClass(error_dd, 'error');
      error_dd.innerHTML = '<strong>'  + error_obj[err] + '</strong>';
      YAHOO.util.Dom.setStyle(error_dd, 'opacity', 0);
      var error_fade_in = new YAHOO.util.Anim(error_dd, {
                                                opacity: { to: 1 }
                                             }, 0.25, YAHOO.util.Easing.easeIn);
      field_container.parentNode.insertBefore(error_dd, field_container);
      error_fade_in.animate();
   }
}

Notice that this is the last item in the ajax_example object, and I’ve left off the comma after it; in theory, JavaScript is like Python, and you can insert a comma after the last item if you like, but in practice that will cause problems in Internet Explorer and Safari. So leave the trailing comma off in this case.

The function itself is pretty simple: each form field in the page is inside a dd, and the id of each is based on the name of the form field it contains; for example, the input for total is in a dd with id total_container. So what the function does is loop over the error messages, and for each one:

  1. Grabs the dd which contains the input, by doing getElementById using the name of the field (which is also the name of the attribute in the error message object which contains the error message, and thus is the “thing” we’re getting in this iteration of the loop).
  2. Builds a new dd with its class set to error, containing the error message, and sets its opacity to zero.
  3. Sets up a “fade in” animation for that dd.
  4. Sticks the new `ddbefore the container for the form field (usinginsertBefore), and fades it in.

And one more thing…

Now we’ve got an object, ajax_example, which contains four functions:

  1. init sets everything up, by grabbing the appropriate element references from the page and setting up an event listener to “hijack” the form.
  2. submit_func temporarily disables the form during submission, and fires off the AJAX request.
  3. ajax_callback handles the AJAX response, and either shows error messages if the form input was incorrect, or displays a “results” div if it wass correct.
  4. display_errors provides the DOM manipulation necessary for ajax_callback to show the error messages.

The only thing left to do is make it all go, so at the very bottom of your JavaScript file, outside the ajax_example object, add this:

YAHOO.util.Event.addListener(window, 'load', ajax_example.init);

Which will ensure that the init function gets called once the page has loaded. That way you don’t have to have any inline JavaScript mized in to your HTML.

And we’re done

Now we have a complete, working example of using AJAX with Django (view source on the example to see the necessary YUI components; the JavaScript we’ve been writing in this example is all in this file), unobtrusively and in a way which degrades perfectly for users who don’t have JavaScript. It took a while to explain everything that was going on, but hopefully you’ll see that, once you wrap your head around asynchronous/event-driven programming, it’s not actually that hard to do — just a little different from what you may be used to.