An Ajax page update mini-tutorial

March 13, 2011 | categories: jQuery, Web, Pyramid, Kotti, Python, JavaScript, Programming | View Comments

Most Ajax applications need to update parts of the DOM or perform some other action after they receive a response for their XHR requests.

This tutorial describes an approach that'll allow you to handle these updates in a unified way, minimzing code duplication and lines of JavaScript code.

(If you're looking at this through your RSS reader, you might want to head over to my blog for some synyax highlighting.)

With jQuery, Ajax functions will typically use a success callback function to give the user some feedback or update parts of the page when the response comes in. A very simple success callback function for an Ajax POST request could look like this:

function success() { alert("Successfully saved.") };

We could then pass this success function to $.post():

$.post('/mypage', {somedata}, success);

Another success handler for a GET request could update parts of the page with HTML sent back from the server:

function success(data) { $("div#content").replaceWith(data) };

In this case, what our server would put in the response would be only the <div> we're interested in:

<div id="content">...some content that's to be updated...</div>

jQuery comes with an Ajax function called .load() that has an implicit success handler which does exactly the same thing:

$('div#content').load('/mynewcontent', {somedata});

Alternatively, and because we're lazy, our server could respond to our XHR request by returning the whole HTML page. We could then extract and update only the bits that we're interested in:

function success(data) {
    var html = $(data);
    $("#message").replaceWith(("#message", html));
    $("#content").replaceWith(("#content", html));
}

This will pick elements #message and #content from the incoming HTML and replace the old contents of those containers on the page. The HTML response for this could look something like this:

<html>
  ...
  <div id="message">Successfully saved.</div>
  ...
  <div id="content">...some content that's to be updated...</div>
  ...
</html>

So far, so good.

Problem

Now imagine that in our application we're using some fancy notifications plug-in that displays messages as a nice pop-up. This is useful when working with Ajax since it'll guarantee that the user actually sees the notification even when they have scrolled way to the bottom of the page. Typically, we'd have some code to turn the contents of <div id="message"> into a pop-up in our document ready handler:

$(document).ready(function() {
    displayNotification($("#message"));
});

What's the problem with this? Well, when we update our <div id="message"> through Ajax, the notifications pop-up won't display. This is because the document ready handler is not triggered for mere updates to the page.

We need to add a call to displayNotification to our Ajax success callback from before to make notifications work for the HTML that we inject dynamically:

function success(data) {
    var html = $(data);
    $("#message").replaceWith(("#message", html));
    $("#content").replaceWith(("#content", html));
    displayNotification($("#message"));
}

Then imagine that you're using more progressive enhancement to turn <ul> list elements elements with a class dropdown into a dropdowns. Again, we need to add a bit of code to both our success handler and to the document ready handler:

$(document).ready(function() {
    displayNotification($("#message"));
    makeDropdowns($("ul.dropdown")); // new!
});

function success(data) {
    var html = $(data);
    $("#message").replaceWith(("#message", html));
    $("#content").replaceWith(("#content", html));
    displayNotification($("#message"));
    makeDropdowns($("ul.dropdown", $("#content"))); // new!
}

Notice how the newly added call to makeDropdowns in function success passes on only ul.dropdown elements inside #content, that is, only the dropdown lists that were injected just now:

makeDropdowns($("ul.dropdown", $("#content")));

By passing only the element that's changed we avoid having to somehow remember in function makeDropdowns which lists were already turned into dropdowns and which not. These takes quite some burden off of these functions.

The problem that emerges as we add these more and more enhancement functions like makeDropdowns and displayNotification is that we keep adding slightly different code to both our document ready handler and to a number of success handlers that we might have created in our app. This sort of duplication of code is bad. Let's try to generalize a bit more.

Solution

Let's first create a unified interface for handlers like displayNotification and makeDropdowns. All of these should have the form:

function handler(node) {
  // do our progressive enhancement here, but only inside node
}

As discussed before, we decide to pass in only the node that has changed. The individual handlers can then apply their enhancements only to the updated parts of the page. We'll then create an array of handlers so that later, when the DOM has been updated, we can call the handlers one by one:

var node_changed_handlers = new Array();
node_changed_handlers.push(displayNotification);
node_changed_handlers.push(makeDropdowns);

function node_changed(node) { // call handlers with 'node' one by one
    $.each(node_changed_handlers, function(index, func) { func(node) });
  }

We can now go back to our document ready handler and to our success handlers and simplify them substantially:

$(document).ready(function() {
    node_changed($('html'));
});

function success(data) {
    var html = $(data);
    $("#message").replaceWith(("#message", html));
    $("#content").replaceWith(("#content", html));
    node_changed($("#message"));
    node_changed($("#content"));
}

No longer do we now need to change the code of both of these whenever we add a new handler to node_changed_handlers. That's good.

Still, writing individual success handlers for all sorts of different actions and Ajax requests, of which there are usually many in a modern web app, is cumbersome. Ideally, our success handler and the server could use some more intelligent protocol that would allow us to reuse the same success callback function for all our application's Ajax requests. In short, we want a success handler that we can use for all our Ajax needs.

In order to achieve this, we add a little bit more information to the HTML that is returned from the server and processed in our handlers. We add a class ajax-replace to all those elements that need to be updated in the page. Here's the example from before with the class added:

<html>
  ...
  <div id="message" class="ajax-replace">Successfully saved.</div>
  ...
  <div id="content" class="ajax-replace"> ... some updated content ... </div>
  ...
</html>

We can now strip out the bit in our success handler that looks for specific ids and generalize it to this:

function success(data) {
    var html = $(data);
    $(".ajax-replace", html).each(function() {
        var selector = "#" + this.id;
        $(selector).replaceWith(this);
        node_changed($(selector));
    });
}

How does this work exactly? It looks for all elements in the server response's HTML with the class ajax-replace (line 3) and replaces elements with a corresponding id in the current DOM (lines 4 and 5). It then calls all node changed handlers with the newly added element (line 6).

Voila! What we have here is a very powerful and simple success handler that's reusable for all our Ajax requests.

Demo

I've recently added Ajax forms and Growl-like notifications (using the jquery-toastmessage-plugin) to the Kotti CMS, a user-friendly light-weight CMS that I'm building on top of Pyramid and jQuery.

Take a look at Kotti's JavaScript code, which implements just the approach I've presented here. In particular, take a look at function messages and function dropdowns which correspond to the two handlers described in this tutorial.

To see this code in action, log in to the Kotti demo server with the username owner and password secret and try the reorder form.

blog comments powered by Disqus