Ajax Application Design

by Reuven M. Lerner

During the past few months, I've used this column to explore a number of technologies and techniques related to Ajax, the asynchronous JavaScript and XML paradigm that is the hottest thing in modern Web development. Everyone is scrambling to include Ajax on his or her sites, and for good reason. For users, Ajax applications appear more responsive and desktop-like. For developers, Ajax is attractive because it breaks the one-page-per-click rule that has existed since the beginning of the Web, making new types of applications possible.

In an Ajax application, a click might force a complete page reload, as in a traditional Web application. But, it might instead fire an HTTP request in the background. The response to this HTTP request is handled (also in the background) by a JavaScript function, which can use the content to modify some or all of the page.

If you have been developing Web applications for a while, you might be wondering what the big deal is with Ajax.> After all, it's neither new nor difficult for a JavaScript function to modify the current page via the DOM, is it? Perhaps not, but sometimes the most powerful ideas result not from fancy technologies, but from the clever combination of simple ones. HTML, HTTP and URLs were all fairly simple inventions, and they might not have gone very far on their own. But by combining them in just the right way, Tim Berners-Lee launched a revolution that continues to this day.

Just as the Web has changed the way that we view publishing and communication, Ajax has changed the way that we expect Web-based applications to work. Fortunately, working with Ajax requires only a few skills above and beyond what Web developers needed to know until now—particularly JavaScript, the DOM and CSS.

Last month, we built a small application that demonstrated the improved usability that Ajax brings to the table. As a visitor filled out the HTML form with a requested user name, a JavaScript function requested (via HTTP) a list of current user names from the server. The HTTP response contained a list of current users. By checking to see whether the newly requested user name was on that list, it was possible to tell the user in advance to choose something else.

This approach had many problems, but the two biggest ones were scalability and security. If our site becomes especially popular, we will have many registered users, so sending a complete list of user names will consume increasing amounts of CPU and bandwidth.

In addition, it is a large security risk to send all of the user names on a site to anyone who requests it. The odds are good that at least one of those users has chosen a poor password, which would make it easy to assume that person's identity. The implications of this security breach depend on your users, your application and your country. Some countries' legal systems might even see this as a prosecutable violation of database privacy laws.

So, for technical and security reasons alike, we need to find a better solution. An obvious candidate, and one we examine this month, involves sending the proposed user name to the server via an Ajax request. The server's response will thus be a short “yes” or “no”, indicating whether the browser should allow or prevent registration.

Ajax Requests

An Ajax application consists of several parts:

  1. A JavaScript function, defined in the Web page, that is invoked when a particular event happens. These event handler functions are common in the JavaScript world, even without Ajax. Before CSS, for example, it was common to use JavaScript to change the src attribute for an img tag whenever the mouse would hover over it (the onmouseover event) or move off of it (the onmouseout event). In the case of Ajax, the event handler function doesn't manipulate the DOM, but rather it sends an asynchronous HTTP request using the XMLHttpRequest object.

    In our example application, the JavaScript function will create an XMLHttpRequest object and use it to invoke a program residing on the server. As a parameter to the request, we will send the contents of the username text field.

  2. A server-side program that expects to receive the HTTP request, along with one or more parameters, and produces an appropriate HTTP response. The response theoretically may be in any legitimate MIME format, although XML, plain text and JSON (JavaScript Object Notation) appear to be the most popular choices. The server-side program will almost certainly not be written in JavaScript. You can choose the language in which you write this program, as well as the method in which it is invoked. The key is that it has access to the resources you need, such as a database, and that it can produce the output in the format you want. In this month's example application, the server-side program takes the username parameter and looks in the database to see if it is already in use. The XML that it returns will indicate its findings.

  3. A second JavaScript function, also defined in the user's Web browser, that is invoked when the HTTP response is received. This callback function, as it is sometimes known, receives the HTTP response and then acts on it. Our callback routine will thus need to parse the Ajax HTTP response and then use the DOM to modify the current page as necessary.

Improving Our Programs

Given the above list, how can we move from the simple program we wrote last month to one that will fulfill our scalability and security requirements?

When we created our simple Ajax user name-checking program in last month's column, we used two of these three elements. We created an HTML form (shown in Listing 1) that would let people register with our Web site by entering a user name, password and e-mail address. We then indicated that whenever the username text field was changed, the checkUsername JavaScript function should be invoked:


   <input type="text" name="username" onchange="checkUsername()" />

checkUsername then asked our server—the same server from which the current page of HTML came—for the contents of a text file:

   function checkUsername() {
      // Send the HTTP request
   xhr.open("GET", "usernames.txt", true);
   xhr.onreadystatechange = parseUsernames;
   xhr.send(null);
   }

This is the first place where we will need to make a change. Rather than send a GET request without any parameters to request a static document, we will send a POST request with a single parameter (username), which will result in the execution of a server-side program.

Finally, our callback routine (parseUsernames) iterated over the list of user names that the server had sent, using the DOM to warn the user if it found a match. This is the other place where we will need to make a change. But in this case, the change will be a simplification. No longer will we need to parse through the user names sent by the server. Instead, we will need to identify only whether the response was positive or negative.

Listing 1. ajax-register.html


   <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
   <html xmlns="http://www.w3.org/1999/xhtml">
   <head><title>Register</title>

      <script type="text/javascript">
      function getXMLHttpRequest () {
         try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {};
         try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e)
   {}
         try { return new XMLHttpRequest(); } catch(e) {};
         return null;
      }

      function removeText(node) {
         if (node != null)
         {
         if (node.childNodes)
         {
               for (var i=0 ; i < node.childNodes.length ; i++)
               {
               var oldTextNode = node.childNodes[i];
               if (oldTextNode.nodeValue != null)
               {
                  node.removeChild(oldTextNode);
               }
               }
         }
         }
      }

         function appendText(node, text) {
               var newTextNode = document.createTextNode(text);
               node.appendChild(newTextNode);
         }


         function setText(node, text) {
               removeText(node);
               appendText(node, text);
         }

         var xhr = getXMLHttpRequest();

         function parseUsernames() {

               // Set up empty array of usernames
               var usernames = [ ];

               // Wait for the HTTP response
         if (xhr.readyState == 4) {
         if (xhr.status == 200) {
               usernames = xhr.responseText.split("\n");
         }
         else
         {
               alert("problem: xhr.status = " + xhr.status);
         }
         }

               // Get the username that the person wants
               var new_username = document.forms[0].username.value;
               var found = false;
               var warning = document.getElementById("warning");
               var submit_button = document.getElementById("submit-button");

         // Is this new username already taken?  Iterate over
         // the list of usernames to be sure.
               for (i=0 ; i<usernames.length; i++)
               {
                  if (usernames[i] == new_username)
                  {
                     found = true;
                  }
               }

         // If we find the username, issue a warning and stop
         // the user from submitting the form.
               if (found)
               {
                  setText(warning, "Warning: username '" + new_username
   +"' was taken!");
                  submit_button.disabled = true;
               }

               else
               {
                  removeText(warning);
                  submit_button.disabled = false;
               }

         }

         function checkUsername() {
               // Send the HTTP request
         xhr.open("GET", "usernames.txt", true);
         xhr.onreadystatechange = parseUsernames;
         xhr.send(null);
         }

      </script>
   </head>
   <body>
      <h2>Register</h2>
      <p id="warning"></p>
      <form action="/cgi-bin/register.pl" method="post">
         <p>Username: <input type="text" name="username"
   onchange="checkUsername()" /></p>
         <p>Password: <input type="password" name="password" /></p>
         <p>E-mail address: <input type="text" name="email_address" /></p>
         <p><input type="submit" value="Register" id="submit-button"
   /></p>
      </form>
   </body>
   </html>

Sending a POST Request

Last month's version of the program sent a GET request. It is possible, and even common, to send one or more parameters with a GET request. Those parameters are then stuck onto the URL, as follows: http://www.example.com/foo.pl?param1=value1&param2=value2.

A separate type of request, known as POST, puts the parameters inside of the request body. This has several advantages, including cleaner URLs and no limit on the length of the parameter names and values. (Many browsers limit the total size of a URL, which includes the parameters for a GET request.)

Although it is not strictly necessary for us to use a POST request for this example program, it is good to see how we can pass parameters in our request. And indeed, it is quite easy to do so. Compare the following code (taken from Listing 2) with the similar excerpt above (from Listing 1):

   function checkUsername() {
      // Send the HTTP request
   xhr.open("POST", "/cgi-bin/check-name-exists.pl", true);
   xhr.onreadystatechange = parseResponse;

   var username = document.forms[0].username.value;
      xhr.send("username=" + escape(username));
   }

As you can see, we have changed the first two parameters to xhr.open to be POST (instead of GET) and to point to a program that will generate dynamic output. The third parameter, which tells the XMLHttpRequest object that it should make the query in the background (that is, asynchronously), remains set to true. I also changed the name of the callback routine to parseResponse, from parseUsername.

The other change is that we are now sending parameters to the server. The variable queryString is just a string consisting of name-value pairs, in the traditional Web format of:


   param1=value1&param2=value2

We thus build such a query string, and send it to the server.

Listing 2. post-ajax-register.html


   <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
   <html xmlns="http://www.w3.org/1999/xhtml">
   <head><title>Register</title>

      <script type="text/javascript">
      function getXMLHttpRequest () {
         try { return new ActiveXObject("Msxml2.XMLHTTP"); } catch(e) {};
         try { return new ActiveXObject("Microsoft.XMLHTTP"); } catch(e)
   {}
         try { return new XMLHttpRequest(); } catch(e) {};
         return null;
      }

      function removeText(node) {
         if (node != null)
         {
         if (node.childNodes)
         {
               for (var i=0 ; i < node.childNodes.length ; i++)
               {
               var oldTextNode = node.childNodes[i];
               if (oldTextNode.nodeValue != null)
               {
                  node.removeChild(oldTextNode);
               }
               }
         }
         }
      }

         function appendText(node, text) {
               var newTextNode = document.createTextNode(text);
               node.appendChild(newTextNode);
         }


         function setText(node, text) {
               removeText(node);
               appendText(node, text);
         }

         var xhr = getXMLHttpRequest();

         function parseResponse() {

               // Get variables ready
               var response = "";
               var new_username = document.forms[0].username.value;
               var warning = document.getElementById("warning");
               var submit_button = document.getElementById("submit-button");

               // Wait for the HTTP response
         if (xhr.readyState == 4) {
         if (xhr.status == 200) {
               response = xhr.responseText;

                     switch (response)
                     {
               case "yes":
                  setText(warning,
                     "Warning: username '" +
                     new_username +"' was taken!");
                  submit_button.disabled = true;
                  break;

               case "no":
                  removeText(warning);
                  submit_button.disabled = false;
                              break;

               case "":
                  break;

               default:
                  alert("Unexpected response '" + response + "'");
                     }

         }
         else
         {
               alert("problem: xhr.status = " + xhr.status);
         }
         }
         }

         function checkUsername() {
               // Send the HTTP request
         xhr.open("POST", "/cgi-bin/check-name-exists.pl", true);
         xhr.onreadystatechange = parseResponse;

         var username = document.forms[0].username.value;
               xhr.send("username=" + escape(username));
         }

      </script>
   </head>
   <body>
      <h2>Register</h2>
      <p id="warning"></p>
      <form action="/cgi-bin/register.pl" method="post"
   enctype="application/x-www-form-urlencoded">
         <p>Username: <input type="text" name="username"
   onchange="checkUsername()" /></p>
         <p>Password: <input type="password" name="password" /></p>
         <p>E-mail address: <input type="text" name="email_address" /></p>
         <p><input type="submit" value="Register" id="submit-button"
   /></p>
      </form>
   </body>
   </html>

The Server Side

Ajax is almost exclusively a client-side paradigm. And, indeed, it is increasingly clear that we can use JavaScript in general, and Ajax in particular, to create new and interesting applications and interfaces. That said, server-side programs still have a major role to play in Web applications, including Ajax applications.

To begin with, only server-side programs can access the site's relational database. (And yes, it's theoretically possible to have JavaScript access the database directly, but that would be a security and performance nightmare.) This means everything you normally would store in a database, but want to have displayed in the browser, will need to be filtered through a server-side program. Almost any nontrivial application will thus benefit from being part of a larger Web framework, such as Zope, Ruby on Rails or even a roll-your-own system that encapsulates behavior in a set of related methods or functions. In other words, the server-side programs in an Ajax application become very specialized database query and reporting tools.

In the interests of time and space, we don't access a database this month. However, there is no way for the HTTP client to know whether the HTTP server is checking a database or returning a random result, and we will take advantage of this secrecy to fudge the lack of a database. If we decide at some point to modify our server-side program to retrieve a list of user names from a database instead of hard-coding the list in a hash, that will be just fine.

Our server-side program, check-name-exists.pl (Listing 3), is a simple CGI program written in Perl. We turn the POSTDATA parameter, which we have received from the Ajax request, and look inside it to see if we received a setting for username. If so, we then look for a match among the keys of the %usernames hash. If we find a match, it returns yes to the caller. If there is no match, it returns no.

Listing 3. check-name-exists.pl

   #!/usr/local/bin/perl

   use strict;
   use diagnostics;
   use warnings;

   use CGI;
   use CGI::Carp;

   # Define the usernames that are taken
   # (Use a hash for lookup efficiency)
   my %usernames = ('abc' => 1,
            'def' => 1,
            'ghi' => 1,
            'jkl' => 1);

   # ------------------------------------------------------------
   my $query = new CGI;
   print $query->header("text/plain");

   # Get the POST data
   my $postdata = $query->param("POSTDATA");

   # Get the username
   my ($name, $value) = split /=/, $postdata;

   my $username = '';
   if ($name eq 'username')
   {
      $username = $value;
   }

   # If this username is defined, say "yes"!
   if (exists $usernames{$username})
   {
      print "yes";
   }

   # Otherwise, say "no"!
   else
   {
      print "no";
   }

Notice how we use a hash, rather than an array, to store the user names. This is a hack for the sake of efficiency; the time it takes to find an array element (and see if there is a match) is proportional to the number of elements in the array. By contrast, hash key lookups take constant time, regardless of how many elements there are. In a production setting, we obviously would expect to look for user names in a database or server-side disk file, rather than a hash or an array.

This example also demonstrates one way to mock up an Ajax application while development is still taking place—create a server-side program that produces results for a very small subset of the data, simulating the full range of database queries that you might normally want to use. In this way, development on the JavaScript side of the project will not have to wait for the server-side portion to be complete, allowing for more parallelized development.

Parsing the Response

When the response arrives from the server, our callback routine, parseResponse, is invoked. As always, we wait until the readyState of our XMLHttpRequest is 4 and for the HTTP status code to be 200. At that point, we can expect one of four different responses from the server:

  • A yes response indicates that the user name was taken. We disable the form's submit button and display a warning. If and when the user changes the text inside of the username text field, the warning will be removed and the submit button re-enabled.

  • A no response indicates that the user name is available. We remove any warning that might have been placed, and enable the submit button.

  • An empty response might come before the yes or no, in which case we ignore it.

  • Finally, it's possible that our program will not behave precisely as we might expect. If this happens, we display the unexpected response that we received for debugging purposes. This is the sort of thing you would probably want to remove from production code.

Notice how we used a switch statement to look at the different possibilities. Also notice how we were able to reduce the complexity of our JavaScript code by sharing the work with the server. This is the key to a good Ajax application. Rather than having the client or the server do all of the work itself, each of them shares in the burden, doing what it can do fastest and most cleanly.

Finally, you might notice that for all of our talk about XML—it is, after all, the x in Ajax—there was a distinct lack of XML in this application. True, we used the XMLHttpRequest to send HTTP requests to the server, but what happened to the XML?

The truth is that Ajax is a great name, but it doesn't quite describe the range of options the programming paradigm provides. The HTTP response, as I indicated above, can come in any MIME type, although XML and plain text are the most common. If this application were returning a more sophisticated set of data, such as a store inventory or points for a chart, XML might be more appropriate. Another format that is gaining in popularity is JSON, which resembles Perl's “Data::Dumper” in its representation of JavaScript objects. Ajax is merely a technique for dividing the work between the client and the server; you should not feel compelled to use XML for the data transfer if it is inappropriate for the task at hand.

Conclusion

This month, we finally produced an application worthy of the Ajax moniker. We used a combination of JavaScript (on the client side) and Perl (on the server side) to check whether a user name was already taken. In doing so, we saw how to use the POST method for submitting data and sent a named parameter to the server. In making these changes, we turned a simple, insecure and unscalable program into a relatively secure and scalable one, without sacrificing the immediate response and interactivity that Ajax brings to the table.

At the same time, you might have noticed our HTML page contained a large number of functions that will be useful for a wide variety of Ajax applications. Starting next month, we will look at some of the open-source libraries that make it easier to create Ajax applications, allowing you to concentrate on the higher-level details.

Reuven M. Lerner, a longtime Web/database consultant, is a PhD candidate in Learning Sciences at Northwestern University in Evanston, Illinois. He currently lives with his wife and three children in Skokie, Illinois. You can read his Weblog at altneuland.lerner.co.il.

Load Disqus comments

Firstwave Cloud