Ajaxifying a Web App: One Pattern at a Time






Ajaxifying a Web App: One Pattern at a Time

Patterns aren't necessarily about big upfront design; more often, they're used for refactoring and enhancing existing systems. The foundational technologies from the previous section can be seen as techniques for Ajaxifying a conventional web app in addition to being useful for building a new one. In the same way, many of the subsequent patterns can be seen as techniques for improving an existing Ajax App.

Here, we'll look at how a few patterns can be progressively applied to enhance a web app. The conventional app is initially described for comparison, but we really begin building the app at Step 1, where we produce a basic Ajax version. In all, there are four steps. In the packaged code, you'll find the complete code for each step. So if you get lost in the middle of Step 2, you can start Step 3 afresh by taking a clean copy of the completed Step 2 directory. Note that all the application files reside in just the one directory.

Background: Old-School Ajaxagram

There is no working directory (no coding required here). See the location of the completed code within the installation package at /tutorial/ajaxagram/. There's an online demo at http://ajaxify.com/tutorial/ajaxagram/.

Starting on familiar ground, the initial version is a conventional web appno Ajax here. It takes a word and lists all anagrams; i.e., all possible combinations of the letters (Figure). For simplicity, there's no dictionary comparison to look for real words, and for the sake of performance, the input field is limited to just five characters. We won't actually build this conventional version, but you might want to peruse the code (in tutorial/ajaxagram/) for comparison with the next Ajaxified version. There are two source files:


anagram.php

The server-side business logic for finding anagrams, described later in the section "Business logic: the anagram web service."


index.phtml

The application's view, a query form, and the list of anagrams. The page submits back to itself; the results area always shows all anagrams arising from the previous form submission (if there was one).

Conventional Ajaxagram


Step 1: Ajaxagram Done Ajax-Style

Ensure you're in the working directory for this demo (/tutorial/ajaxagram/; this is the same for all steps). Note that you'll find completed code for this step within the installation package under directory /tutorial/ajaxagram/ajaxified/, and an online demo for this step at http://ajaxify.com/tutorial/ajaxagram//ajaxified.

The initial Ajax version looks and feels just like the conventional version. The only difference is that there's no page refresh as the query word is passed via web remoting [see the Web Remoting (Chapter 6) patterns], and the results updated using display manipulation [see the Display Manipulation (Chapter 5) patterns]. As an overview, we'll be creating four files inside the one directory:


anagram.php

The server-side business logic for finding anagrams; described in the next section.


anagram.phtml

A server-side web service; accepts a word and outputs a comma-separated list of anagrams.


index.html

The browser-side application's view; a plain HTML file that contains the form and list of anagrams.


ajaxagram.js

The browser-side application logic; waits for a submission, passes the query word to anagram.phtml, and alters the display accordingly.

Begin the application by creating a fresh directory; e.g., tutorial/ajaxagram/ (you'll need to create tutorial/ if you didn't already do so).

  mkdir tutorial
  cd tutorial
  mkdir ajaxagram

For Unix systems, ensure permissions are appropriate for your web server (e.g., make the directories globally readable and executable). All the files throughout the four steps described next should go in this directory.

Business logic: the anagram web service

We'll begin the web service by creating a backend module that knows only the business logic related to anagrams, and nothing about HTML. Since we're talking pure business logic, I'll just show you the entire file and skim over the details. Create the following as anagram.php (or copy it from the code package):

  <?

    /* Cleans input word and passes into findAnagramsRecursive */
    function findAnagrams($word) {
      $sorted = str_split(filterInput($word));
      sort($sorted);
      return findAnagramsRecursive(implode($sorted));
    }

    /* Convert to lowercase and only include letters */
    function filterInput($word) {
      $word = strtolower($word);
      $word = ereg_replace("[^a-z]","", $word);
      return $word;
    }

    /* Assumes $word is sorted */
    function findAnagramsRecursive($word) {

      if (strlen($word)==0) { return array( ); }
      if (strlen($word)==1) { return array($word); }

      // For each character in the word, extract it and find all anagrams
      // where it's the first character.
      $anagrams = array( );
      for ($pos=0; $pos<strlen($word); $pos++) {

        $extractedChar = substr($word,$pos,1);
        // If same as previous, don't process any further as it will only
        // create duplicates (this is the only check against duplicates,
        // since the word is sorted).
        $sameAsPreviousChar =
          ($pos > 0 && $extractedChar==substr($word,$pos-1,1));

        if (!$sameAsPreviousChar) {
          $remaining = removeCharAt($word, $pos);
          $anagramsOfRemaining = findAnagramsRecursive($remaining);
          foreach ($anagramsOfRemaining as $anagramOfRemaining) {
            array_push($anagrams, $extractedChar . $anagramOfRemaining);
          }
        }
      }

      return $anagrams;

    }

    /* Return word without character at pos */
    function removeCharAt($word, $pos) {
      return substr($word, 0, $pos) . substr($word, $pos+1);
    }

  ?>

There's also a packaged test case, AnagramTest.php; it's good practice to test backend business logic in isolation. If you want to run the test, install PHPUnit2 (http://www.phpunit.de/pocket_guide/3.0/en/index.html) first and see its documentation for details on running tests.

With the business logic now in the server, we need a thin wrapper to expose it as a clean Web Service. Placing it in a separate file helps separate business logic from concerns about how it will be invoked from the browser. Thus, create the web service anagrams.phtml as follows:

  <?
    require_once("anagram.php");

    $word = array_key_exists('word', $_GET) ? $_GET['word'] : "";
    $anagramsArray = findAnagrams($word);
    print implode(",", $anagramsArray);
  ?>

You can test the web service: point your browser at http://localhost/tutorial/ajaxagram/anagrams.phtml?word=ajax, and you should see a comma-separated list of "ajax" anagrams. Notice that this is a raw response intended for easy script processing and not the sort of thing you'd show a user. Our script will need to parse it in order to present the results to users.

Presentation: initial HTML

The initial HTML file contains the basic display detaila form and a list of anagramsand also pulls in the required JavaScript. Create index.html as follows:

  <html>

  <head>
    <title>AjaxPatterns.org - Ajaxagram (Basic Ajax Version)</title>
    <script type="text/javascript" src="ajaxagram.js"> 
 
</script>
  </head>

  <body>

    <h1>Ajaxagram (Basic Ajax Version)</h1>

    <div>
      <input type="text" id="word" size="5" maxlength="5" />
      <button id="findAnagrams">Find Anagrams!</button>
    </div>

    <div id="results">
    </div>

  </body>

Point your browser to the HTML; you should see the heading and the form, but probably not the results area as it's initially empty.

There are a few things to note about the HTML, and you might like to contrast it with index.phtml packaged in the conventional version. First, the "form" is not a true HTML <form> element because we don't need the browser to submit it to the server; our script will issue the query manually. Second, as there's no HTML form here, a button element is used instead of a submit control. Third, the results div is empty. It will be populated each time our script receives a list of anagrams from the server. Finally, note the absence of any JavaScript code here; all the application logic is isolated in a separate file, ajaxagram.js.

Application logic: JavaScript

The final task is coding the application logic that acts as the glue between the server-side web service and the browser-side web page. Create ajaxagram.js with the following generic methods, explained earlier in the "In A Blink" tutorials. Keep these at the end of the file as we build the rest of the JavaScript.

// -- Generic functions --------------------------------------------------

function createXMLHttpRequest( ) {
      try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch (e) {}
      try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {}
      try { return new XMLHttpRequest( ); } catch(e) {}
      alert("XMLHttpRequest not supported");
      return null;
}

function $(id) {
  return document.getElementById(id);
}

The JavaScript needs to handle the query lifecycle, which has three stages:

  1. The user clicks the findAnagrams button.

  2. XMLHttpRequest submits word input to the anagarams.phtml web service.

  3. When the anagram list arrives from server, the anagrams block is updated.

We'll now create three JavaScript functions, with each function roughly corresponding to each of these stages. The first of these ensures the findAnagrams button will trigger the query:

  window.onload = function( ) {
    $("findAnagrams").onclick = function( ) {
      submitWord( );
    }
  }

If you like, you can test the application now. Upon submitting, you should see an error message because submitWord( ) is not there yet. We'll create submitWord( ) now, using essentially the same boilerplate code as in the earlier "Web Remoting in a Blink" tutorial.

  function submitWord( ) {
    var xhr = createXMLHttpRequest( );
    xhr.onreadystatechange = function( ) {
      if (xhr.readyState==4) { // Request is finished
        if (xhr.status==200) {
          var anagramsCSV = xhr.responseText;
          updateAnagramsDisplay(anagramsCSV);
        } else {
          alert("An error occurred.");
        }
      }
    }
    var word = $("word").value;
    xhr.open("GET", "anagrams.phtml?word="+word, true);
    xhr.send(null);
  }

After adding submitWord( ), the browser submits the query word to anagrams.phtml. You can check it by clicking on the Find Anagrams! button and verifying that your web server logs the call to anagrams.phtml. You might also monitor the browser-server traffic using a tool mentioned in Traffic Sniffing (Chapter 18). Once the script has returned successfully, updateAnagramsDisplay( ) is called.

updateAnagramsDisplay( ) receives the comma-separated list of anagrams, splits it to form an array, and outputs each anagram on a separate line:

  function updateAnagramsDisplay(anagramsCSV) {
    $("results").innerHTML = "";
    var anagrams = anagramsCSV.split(",");
    for (var i=0; i<anagrams.length; i++) {
      $("results").innerHTML += anagrams[i] + "<br/>";
    }
  }

Ajaxagram should now work as described earlier. Congratulations, you've built a working Ajax application! You built it using several of the patterns described in the first part of the Ajax Patterns languagethe patterns of foundational technologies. Those patterns are enough to build an application, but the patterns of the latter sections consider real-world issues like usability and maintainability. Those are illustrated in the next few sections.

Incidentally, you might notice a small bug. If you search too quickly, you'll see the results for different searches blending together. This is a consequence of the asynchronous nature of Ajax, with results being processed simultaneously in the browser. We'll eventually fix the problem in Step 3, where we introduce one of several possible techniques to prevent issues like this.

Step 2: Enhancing Functionality and Usability

Ensure you're in the working directory for this demo (/tutorial/ajaxagram/; this is the same for all steps). Note that you'll find completed code for this step within the installation package under directory /tutorial/ajaxagram/ajaxified/richer, and an online demo for this step at http://ajaxify.com/tutorial/ajaxagram/ajaxified/richer/.

So far, Ajaxagram is impressive insofar as you can now tell your tech friends you've coded an Ajax application. Still, your boss/client/shareholder is going to have a hard time telling it apart from the conventional version. We're going to have to jazz the user interface up if we want to make a true impression. (And hopefully improve usability in the process.) We'll refactor according to Functionality and Usability Patterns (Part IV). By the way, we're only "refactoring by patterns" for demonstration purposes; in real life, it's a lot wiser to let the requirements drive the changes you make and let the patterns follow from there.

Note that the tutorial code package includes the completed files for each step. If you had any problems with the initial version, simply start from here using the files in /tutorial/ajaxagram/ajaxified/.

Live Search

We'll begin the UI face-lift by making the input mechanism become a Live Search (Chapter 14). No more pressing the Find Anagrams! button because the results will update on each keystroke. Incidentally, Live Search needs to be applied with caution in the real world, because it can sometimes be more of an annoyance than an aid.

Where we previously submitted upon button click, we now submit upon each keypress:

  window.onload = function( ) {
    $("word").onkeyup = function( ) {
      submitWord( );
    }
  }

And that's that! Reload the page and we now have a working Live Search. Frankly, there's more work required to make it production-ready, but it's pretty neat that our basic Ajax code could be modified so easily to do it. Complete the Live Search by scrapping the Find Anagrams! button, which is now obsolete. This will leave the input area as follows:

  <div>
    <input type="text" id="word" size="5" maxlength="5" />
  </div>

Progress Indicator

The input field is limited to five characters because long queries take a very long time. But even with five characters, the delay will be noticeable, especially in a real-world context, so we'll ease the pain with a Progress Indicator (Chapter 14). This is an animated GIF that's always present on the page, with its visibility toggled on and off, depending on whether we're waiting for a response.

  <div>
    <input type="text" id="word" size="5" maxlength="5" />
  </div>

  <img id="progress"
   src="http://ajaxify.com/tutorial/resources/progress.gif"
         class="notWaiting"/>

You can leave the image URL pointing to my ajaxify.com serveryou're welcome to do so and it will work fineor alternatively, download the image (or copy it from the code package tutorial/resources/) to the current directory and reference it locally (using src="progress.gif").

Next, we're going to introduce a CSS file with the styles for waiting and notWaiting modes. Just as we've separated JavaScript from HTML, we'll also separate CSS. To that end, add the following line near the top of index.html:

  <head>
    <title>AjaxPatterns.org - Ajaxagram (Basic Ajax Version)</title>
    <script type="text/javascript" src="ajaxagram.js"></script>
    <link rel="stylesheet" type="text/css" href="ajaxagram.css"/>
  </head>

Now create the new CSS file, ajaxagram.css, in the same directory.

  .waiting {
    visibility: visible;
  }

  .notWaiting {
    visibility: hidden;
  }

Now we can just flip the image's CSS class between waiting and notWaiting, according to the request state. The image enters waiting mode when a query is submitted, in the submitWord( ) method, and enters notWaiting when the query returns.

  function submitWord( ) {
    // Show progress indicator and clear existing results
    $("progress").className = "waiting";
    $("results").innerHTML = "";
    ...
  }

  function updateAnagramsDisplay(anagramsCSV) {
    $("progress").className = "notWaiting";
    var anagrams = anagramsCSV.split(",");
    ...
  }

At this stage, you should see the Progress Indicator while waiting (Figure). If the wait is too short to see it, buy yourself a slower PC or alternatively, instrument anagrams.phtml with a delay; e.g., sleep(1);. That's a basic illustration of the Progress Indicator pattern. For a harder problem, increase the width of the input field to allow longer anagrams, and then change the Progress Indicator to show percent complete. (Hint: use a Guesstimate [Chapter 13] or create a monitoring channel.)

Ajaxagram with Progress Indicator


One-Second Spotlight (Yellow Fade Technique)

Along with Progress Indicators, one of Ajax's great visual landmarks is the Yellow Fade Technique, or the more general One-Second Spotlight (Chapter 16) pattern. Here, we're going to fade results in each time they update. The div will suddenly become yellow and gradually settle back to white over the space of a second. It will happen frequently enough to be considered an abuse of the technique, but hey, it's nice eye candy and good enough for demo purposes.

The fade animation here is super-simplistic. In 10 steps, each 100 milliseconds apart, it pumps up the RGB value from (100%, 100%, 0%) to (100%, 100%, 100%). That is, from yellow to white. Thus, red and green components of the div color are fixed at 100 percent, while blue increases by 10 percent each iteration:

  // Caller-friendly front-end to fadeLoop
  function fade( ) {
    fadeLoop(0);
  }

  function fadeLoop(nextBluePercent) {
    $("results").style.backgroundColor = "rgb(100%, 100%, "+nextBluePercent+"%)";
    nextBluePercent += 10;
    if (nextBluePercent<=100) {
      setTimeout("fadeLoop("+nextBluePercent+")", 100);
    }
  }

Invoke fade( ) just prior to outputting the fresh anagram list:

  function updateAnagramsDisplay(anagramsCSV) {
    $("progress").className = "notWaiting";
    fade(0);
    ...
  }

Now click on Find Anagrams! and enjoy the hypnotic Ajax fade effect (Figure).

Yellow Fade Effect (One-Second Spotlight) in Ajaxagram


Step 3: Refactoring for Performance

Ensure you're in the working directory for this demo (/tutorial/ajaxagram/; this is the same for all the steps). Note that you'll find completed code for this step within the installation package under directory /tutorial/ajaxagram/ajaxified/richer/performant/, and an online demo for this step at http://ajaxify.com/tutorial/ajaxagram/ajaxified/richer/performant/.

The previous section shows how the Ajax Patterns go beyond foundational technologies to look at usability and functionality issues. That alone would be useful, but we still need to address the nonfunctional aspects of a rich web application, like performance, data integrity, testability, and programmer productivity. That's the domain of Programming Patterns (Part III), as well as Development Patterns (Part V). Here we'll treat the application with Submission Throttling, a programming pattern.

The problem we're dealing with arises from the Live Search capability. Responding to each keystroke is nice, but unfortunately impractical in most real-world settings. We want a way to limitthrottlethe submissions. The solution is very simple: submit only after an idle period of one second. Modify ajaxagram.js as follows:

  var submitTimer;

  window.onload = function( ) {
    $("word").onkeyup = function( ) {
      clearTimeout(submitTimer); // Clears any previously
scheduled submission
    }
  }

Reload the application and you should see the delay between typing the word and seeing the results appear. We've now capped the number of browser-server calls to one per second, which is one style of Submission Throttling. It works using timers; as explained in the Scheduling (Chapter 7) pattern, JavaScript lets us set a one-off event in the future and cancel it too. The above code ensures that each keystroke schedules a call in one second and cancels any pending call.

Step 4: Refactoring to an External Library

Go to the working directory (/tutorial/ajaxagram/; this is the same for all the steps). Note that you'll find completed code within the installation package under directory /tutorial/ajaxagram/ajaxified/richer/performant/library/, and on online demo for this step at http://ajaxify.com/tutorial/ajaxagram/ajaxified/richer/performant/library/.

The final refactoring isn't actually related to a particular pattern, but is a reminder of the importance of libraries and frameworks throughout the patterns. While most patterns cover low-level JavaScript concepts that are worth knowing, that doesn't mean you have to hand-code everything yourself. There's an ever-growing list of production-ready components available, as documented in Appendix A. The demo here shows how we can achieve the same functionality by reusing a library.

A prime candidate is the remoting performed by Ajaxagram. There's very little reason to directly use XMLHttpRequest nowadays, so we'll refactor to one of the many libraries that wraps it in a clean API. We'll use ajaxCaller (http://www.ajaxify.com/run/testAjaxCaller/), a small component developed specifically for the demos in this book.

Add a new <script> tag to index.html to include the library:

  <head>
  <title>AjaxPatterns.org - Ajaxagram ()</title>
  <script type="text/javascript" src="ajaxagram.js"></script>
  <script type="text/javascript" src="http://ajaxify.com/tutorial/resources/
       ajaxCaller.js"></script>
 <link rel="stylesheet" type="text/css" href="ajaxagram.css" />
</head>

The above line will pull the script down from my ajaxify.com server; as with the progress indicator earlier, you're welcome to leave it like that.[*] Alternatively, you can download the script (or copy it from the code package tutorial/resources)/ to the current directory and change the URL accordingly (use src="ajaxCaller.js").

[*] As explained in the On-Demand JavaScript (Chapter 6) pattern, it's perfectly feasible to reference an external JavaScript file. However, beware that there's always some risk involved as you're executing code from a server you have no control over.

submitWord( ) now becomes almost trivial:

  function submitWord( ) {

    // Show progress indicator and clear existing results
    $("progress").className = "waiting";
    $("results").innerHTML = "";

    var word = $("word").value;
    ajaxCaller.getPlainText("anagrams.phtml?word="+word,updateAnagramsDisplay);
  }

In addition, you can remove the generic createXMLHttpRequest( ) function. That's a lot of code we no longer need thanks to the external library.

Refactoring Exercises

The final Ajaxagram makes a simple platform on which to play around with further patterns. Here are a few exercises you can try out. Let me know how it goes (contact me at [email protected]). I'd be pleased to link to any examples from the tutorial's homepage.

Foundational technology patterns

On-Demand JavaScript (Chapter 6)

Instead of downloading all of ajaxagram.js at the start, simply download the bootstrapping code and have it fetch the rest of the script on demand.

Programming patterns

Web Services (Chapter 9) patterns

Experiment with the response emerging from the anagrams.phtml web service. Try returning an HTML Message (Chapter 9) or an XML Message (Chapter 9) instead.


Call Tracking (Chapter 10)

As the first step mentioned, the anagrams list sometimes contains results for several queries at once. The refactoring to the Submission Throttling (Chapter 10) pattern basically fixed this by enforcing a gap between submissions. However, you don't always want to use Submission Throttling; even if you do, there's no guarantee it will solve this problem. Introduce Call Tracking for a more direct attack.


Submission Throttling (Chapter 10)

We applied one kind of Submission Throttling, but try for the kind discussed in the Code Example section of Chapter 10.


Cross-Domain Proxy (Chapter 10)

Instead of calling your own server for the anagrams, relay the call to my server at ajaxify.com/tutorial/ajaxagram/ajaxified/anagrams.phtml.


Browser-Side XSLT (Chapter 11

Transmit the anagrams in XML format and convert them with XSLT for display.


Browser-Side Templating (Chapter 11)

Show the results using a template framework.


Browser-Side Cache (Chapter 13)

Cache previous anagram lists.


Predictive Fetch (Chapter 13)

For the basic version, which uses a button for submission, make a pre-emptive call in anticipation of a button press.


Fat Client (Chapter 13)

Forget the server altogether and compute the anagrams in the browser.

Functionality and Usability patterns

Data Grid (Chapter 14)

Show the list of anagrams in a Data Grid, allowing for search, filtering, and so on.


Suggestion (Chapter 14)

Suggest words from a dictionary as the user types.


On-Demand JavaScript (Chapter 6)

Instead of downloading all of ajaxagram.js at the start, simply download the bootstrapping code and have it fetch the rest of the script on demand.


Popup (Chapter 15)

Produce the results in a Popup.


Portlet (Chapter 15)

Encapsulate the search as a Portlet and slap it on a web site you maintain.


Virtual Workspace (Chapter 15)

The demo produces many, many results for long queries, which is why the input field is limited to five letters. Allow more letters and show the results as a Virtual Workspace. You'll probably want to make the algorithm itself more flexible.


Unique URLs (Chapter 17)

Make sure each query corresponds to a unique URL.

Development patterns

Diagnosis (Chapter 18) patterns

Apply the various tools mentioned in this chapter.


Service Test (Chapter 19)

Create a standalone HTTP client to test anagrams.phtml. Note that AnagramTest already tests the backend logic, so the most important thing is simply to check the service interface works.


System Test (Chapter 19)

Apply the various tools mentioned in System Test.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows