Providing brighter futures through tutoring
rss        info        email
email        info        rss
 

Ajax in a Nutshell

Accessing server-based web applications from browsers originally involved inputting content via an HTML form's fields, clicking a submit button to send this content to the web app, and receiving (from the web app) a new page whose content replaced the form page.

This experience proved to be less elegant than that of interacting with desktop programs, which "stay on the same page." It also felt sluggish due to server-processing delays, network latency, and other delays between form submission and new page response. Ajax was created to improve this experience.

What is Ajax?

Ajax, an acronym for Asynchronous JavaScript and XML, is a group of interrelated web technologies (JavaScript, XMLHttpRequest, XML, DOM, HTML, CSS) used on the browser side to create interactive web apps. Browsers use Ajax to send data asynchronously (or even synchronously) to servers so that web pages don't need to be reloaded.

Data is sent via JavaScript and XMLHttpRequest; retrieved via these technologies, XML, and the DOM; and marked up and styled via HTML and CSS. Returned data consists of an XML document or plain text. If plain text is returned, it may be formatted as JavaScript Object Notation (JSON), and subsequently evaluated within JavaScript to create a DOM-based data object.

What is XMLHttpRequest?

XMLHttpRequest is a DOM API that's accessed from a scripting language (JavaScript, VBScript, and so on), via a typically same-named XMLHttpRequest object, to send HTTP/HTTPS requests to a web app and retrieve application response data for processing and presentation.

The following steps are required to work with the XMLHttpRequest object:

  1. Detect the presence of the XMLHttpRequest object. This can be accomplished by invoking the following JavaScript code:

    var xmlHttpReq;
    
    if (window.XMLHttpRequest)
    {
       // The following is supported by Firefox, Chrome, Opera, Safari, and IE7+.
    
       xmlHttpReq = new XMLHttpRequest();
    }
    else 
    if (window.ActiveXObject)
    {
       // The following is supported by IE5 and IE6.
    
       xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
    }
    else
    {
       alert("Your browser doesn't support XMLHttpRequest!");
    }

    A browser that supports XMLHttpRequest will indicate this support by providing XMLHttpRequest as a property of the DOM window object. Because XMLHttpRequest wasn't standardized when IE5 and IE6 were released, this object must be obtained via ActiveX on these browsers.

  2. Assign a function to XMLHttpRequest's onreadystatechange property. This function is invoked (only for asynchronous requests) whenever XMLHttpRequest's readyState property changes. I'll discuss this topic later in this article.

  3. Prepare to send an HTTP/HTTPS request to a web app by invoking XMLHttpRequest's open() function. The first argument passed to this function is a string that identifies the HTTP request method, typically GET or POST. The second argument is a string identifying the web app via a URL. Three additional (and optional) arguments (a Boolean flag indicating a synchronous request rather than the default asynchronous request, a user ID, and a password) may be passed (as necessary).

    For security reasons, the URL must not specify protocol, host, and/or port components that differ from the equivalent components in the invoking document's URL. The browser is expected to raise a security error when this rule is violated. This requirement may be relaxed in a future version of the XMLHttpRequest API.

  4. Invoke XMLHttpRequest's setRequestHeader() function to initialize any required HTTP headers before making the request. The first argument passed to this function is a string containing the header name. The second argument is a string containing the header value. Each invocation initializes a single header. Also, these function calls must be made after each open() call -- they aren't cached.

  5. Invoke XMLHttpRequest's send() function to send the request. Content may be sent to the web app by specifying it as this function's solitary argument, but it's common to pass null to indicate that content is being sent via an HTTP request method.

    For asynchronous requests (the default), this function returns immediately. The script obtains returned data from within the function assigned to onreadystatechange. For synchronous requests (true was passed to open() as the third argument, for browsers that support this kind of request), send() blocks until the request finishes.

During an asynchronous request, the function assigned to onreadystatechange is invoked whenever the readyState property is changed:

  • Following a successful call to open(), 1 is assigned to readyState.
  • Following a call to send() and the retrieval of HTTP response headers, 2 is assigned to readyState.
  • Once HTTP request content starts loading, 3 is assigned to readyState.
  • After HTTP request content finishes loading, 4 is assigned to readyState.

Code within the function assigned to onreadystatechange typically checks readyState to see if it equals 4. In this case, the code can proceed to obtain and process the returned content. Similarly, after a synchronous send() call completes, code following this call can obtain and process the returned content.

When the returned data is a valid XML document, XMLHttpRequest's responseXML property contains a DOM object that provides access to this content via its methods. Also, the responseText property usually contains the response in plain text, regardless of the content being sent back as an XML document.

The following JavaScript code reveals the general architecture for detecting XMLHttpRequest and using this object to send an asynchronous request to a web app:

var xmlHttpReq;

if (window.XMLHttpRequest)
{
   // The following is supported by Firefox, Chrome, Opera, Safari, and IE7+.

   xmlHttpReq = new XMLHttpRequest();
}
else 
if (window.ActiveXObject)
{
   // The following is supported by IE5 and IE6.

   xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
}
else
{
   alert("Your browser doesn't support XMLHttpRequest!");
   return;
}

var url = ... // web application's URL
xmlHttpReq.onreadystatechange = stateChanged;
xmlHttpReq.open("GET", url);
xmlHttpReq.send(null);

function stateChanged()
{
   if (xmlHttpReq.readyState == 4)
   {
      // Process the returned data.
   }
}

You'll encounter this architecture in the following Ajax examples, which count vowels and consonants, fetch a calendar for a specific year and month, and fetch zip code-specific weather information.

Vowels and Consonants

Our first Ajax example shows you how to use Ajax to count vowels and consonants as characters are entered into a textfield. For each entered character, a PHP script is invoked to compute these metrics, which are then displayed below the textfield. Figure 1 reveals this example in action.

Vowels and consonants are counted and these counts are
output as text is entered.

Figure 1: Vowels and consonants are counted and these counts are output as text is entered.

This example relies on Listing 1, which includes a JavaScript source file that defines a function named countVaC(), and which specifies an HTML form that invokes this function.

<script type="text/javascript" src="vac.js">
</script>

<form action="">
  <p>
  <label>
    Enter some text:
  </label>

  <input type="text" onkeyup="countVaC(this.value)">
</form>

<p>Vowels and consonants: <span id="vac"></span></p>

Listing 1: Client-side HTML for this example

As keys are pressed, this form's textfield's onkeyup event handler invokes countVaC(this.value) to pass the textfield's contents to this function, which Listing 2 reveals.

function countVaC(str)
{
   var xmlHttpReq;

   if (window.XMLHttpRequest)
   {
      // The following is supported by Firefox, Chrome, Opera, Safari, and IE7+.

      xmlHttpReq = new XMLHttpRequest();
   }
   else 
   if (window.ActiveXObject)
   {
      // The following is supported by IE5 and IE6.

      xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
   }
   else
   {
      alert("Your browser doesn't support XMLHttpRequest!");
      return;
   }

   var url = "http://tutortutor.ca/cgi-bin/vac.php";
   url = url+"?txt="+str;
   xmlHttpReq.onreadystatechange = stateChanged;
   xmlHttpReq.open("GET", url);
   xmlHttpReq.send(null);

   function stateChanged()
   {
      if (xmlHttpReq.readyState == 4)
      {
         document.getElementById("vac").innerHTML = xmlHttpReq.responseText;
      }
   }
}

Listing 2: vac.js

After obtaining the XMLHttpRequest object, countVaC() builds a URL that identifies the PHP script (see Listing 3) to execute along with its str argument. It then assigns a function that processes the script's returned text to onreadystatechange, invokes open() to identify the GET request method and the script's URL, and invokes send() to launch the script.

<?php

$nvowels = 0;
$nconsonants = 0;

// Get the txt parameter from the URL.

$txt = $_GET["txt"];

// Scan txt for vowels and consonants.

for ($i = 0; $i < strlen($txt); $i++)
{
   $ch = substr($txt, $i, 1);
   if ($ch == 'a' || $ch == 'A' || $ch == 'e' || $ch == 'E' ||
       $ch == 'i' || $ch == 'I' || $ch == 'o' || $ch == 'O' ||
       $ch == 'u' || $ch == 'U' || $ch == 'y' || $ch == 'Y')
       $nvowels++;
   else
   if (($ch >= 'B' && $ch <= 'Z') || ($ch >= 'b' && $ch <= 'z'))
       $nconsonants++;
}

echo "Vowels = $nvowels; Consonants = $nconsonants";
?>

Listing 3: vac.php

When send() returns, countVaC() exits -- it's reinvoked when a key is pressed. Meanwhile, stateChanged() is asynchronously invoked. When this function detects a readyState value of 4, it embeds the script's response text in Listing 1's vac span (an HTML element for adding inline structure to a document).

The response text is embedded in this span via document.getElementById("vac").innerHTML = xmlHttpReq.responseText;. After document.getElementById("vac") uses the DOM to obtain the HTML element object whose CSS ID is vac, the text is embedded into this element by assigning xmlHttpReq.responseText to the object's innerHTML property.

Calendar

In our second Ajax example, you select a month and year from a form's dropdown months and years listboxes. For each selection, a PHP script is invoked to create a calendar for that month/year, and the resulting calendar is displayed below these listboxes. Figure 2 reveals a sample calendar.

A calendar is displayed whenever you select a month or
a year.

Figure 2: A calendar is displayed whenever you select a month or a year.

This example relies on Listing 4, which includes a JavaScript source file that defines a function named obtainCal(), and which specifies an HTML form that invokes this function.

<script type="text/javascript" src="cal.js">
</script>

<form action="">
  <p>
  <label>
    Month
  </label>

  <select name="months" onchange="obtainCal(months.value, years.value)">
    <option value="0">---</option>
    <option value="1">January</option>
    <option value="2">February</option>
    <option value="3">March</option>
    <option value="4">April</option>
    <option value="5">May</option>
    <option value="6">June</option>
    <option value="7">July</option>
    <option value="8">August</option>
    <option value="9">September</option>
    <option value="10">October</option>
    <option value="11">November</option>
    <option value="12">December</option>
  </select>

  <label>
    Year
  </label>

  <select name="years" onchange="obtainCal(months.value, years.value)">
    <option value="0">---</option>
    <option value="2000">2000</option>
    <option value="2001">2001</option>
    <option value="2002">2002</option>
    <option value="2003">2003</option>
    <option value="2004">2004</option>
    <option value="2005">2005</option>
    <option value="2006">2006</option>
    <option value="2007">2007</option>
    <option value="2008">2008</option>
    <option value="2009">2009</option>
    <option value="2010">2010</option>
    <option value="2011">2011</option>
  </select>
</form>

<p>Calendar:</p>

<div id="cal">
</div>

Listing 4: Client-side HTML for this example

As listbox selections are made, the form's selected listbox's onchange event handler invokes obtainCal(months.value, years.value) to pass the listbox selections to this function, shown in Listing 5.

function obtainCal(month, year)
{
   var xmlHttpReq;

   if (window.XMLHttpRequest)
   {
      // The following is supported by Firefox, Chrome, Opera, Safari, and IE7+.

      xmlHttpReq = new XMLHttpRequest();
   }
   else 
   if (window.ActiveXObject)
   {
      // The following is supported by IE5 and IE6.

      xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
   }
   else
   {
      alert("Your browser doesn't support XMLHttpRequest!");
      return;
   }

   var url = "http://tutortutor.ca/cgi-bin/cal.php";
   url = url+"?month="+month+"&year="+year;
   xmlHttpReq.onreadystatechange = stateChanged;
   xmlHttpReq.open("GET", url);
   xmlHttpReq.send(null);

   function stateChanged()
   {
      if (xmlHttpReq.readyState == 4)
      {
         document.getElementById("cal").innerHTML = xmlHttpReq.responseText;
      }
   }
}

Listing 5: cal.js

After obtaining the XMLHttpRequest object, obtainCal() builds a URL that identifies the PHP script (see Listing 6) to execute along with its month and year arguments. It then assigns a function that processes the script's returned text to onreadystatechange, invokes open() to identify the GET request method and the script's URL, and invokes send() to launch the script.

<?php

// Get the month parameter from the URL. Abort if 0 passed.

$month = (int) $_GET["month"];
if ($month == 0)
   return;
$month--;

// Get the year parameter from the URL. Abort if 0 passed.

$year = (int) $_GET["year"];
if ($year == 0)
   return;

// Create the calendar.

$daysInMonth = array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);

if (isleap($year))
   $daysInMonth[1]++;

$maxDays = $daysInMonth[$month];

$months = array("January", "February", "March", "April", "May", "June",
                "July", "August", "September", "October", "November",
                "December");

echo "<table border=1>";
echo "<th bgcolor=#eeaa00 colspan=7>";
echo "<center>".$months[$month]." $year</center>";
echo "</th>";

echo "<tr bgcolor=#ff7700>";
echo "<td><b><center>S</center></b></td>";
echo "<td><b><center>M</center></b></td>";
echo "<td><b><center>T</center></b></td>";
echo "<td><b><center>W</center></b></td>";
echo "<td><b><center>T</center></b></td>";
echo "<td><b><center>F</center></b></td>";
echo "<td><b><center>S</center></b></td>";
echo "</tr>";

$dayOfWeek = date("w", mktime(12, 0, 0, $month+1, 1, $year));

$day = 1;
for ($row = 0; $row < 6; $row++)
{
   echo "<tr>";
   for ($col = 0; $col < 7; $col++)
   {
      if (($row == 0 && $col < $dayOfWeek) || $day > $maxDays)
      {
         echo "<td bgcolor=#cc6622>";
         echo " ";
      }
      else
      {
         if ($day%2 == 0)
            echo "<td bgcolor=#ff9940>";
         else
            echo "<td>";
            echo $day++;
      }
      echo "</td>";
   }
   echo "</tr>";
}

echo "</table>";

function isleap($year)
{
   return (!($year & 3) && $year%100 || !($year%400));
}
?>

Listing 6: cal.php

When send() returns, obtainCal() exits -- it's reinvoked when a selection is made. Meanwhile, stateChanged() is asynchronously invoked as described earlier. When this function detects a readyState value of 4, it embeds the script's response text in Listing 4's cal div (an HTML element for adding block structure to a document).

The response text is embedded in this div via document.getElementById("cal").innerHTML = xmlHttpReq.responseText;. After document.getElementById("cal") uses the DOM to obtain the HTML element object whose CSS ID is cal, the text is embedded into this element by assigning xmlHttpReq.responseText to the object's innerHTML property.

Weather Information

In our final Ajax example, you enter a United States zip code into a textfield and click a button. In response, a PHP script is invoked to obtain the most recent weather information for that zip code (as an XML document) from Yahoo! Weather. Figure 3 reveals weather information for a famous zip code.

The latest weather details are presented for the
entered zip code.

Figure 3: The latest weather details are presented for the entered zip code.

This example relies on Listing 7, which includes a JavaScript source file that defines a function named wi(), and which specifies an HTML form that invokes this function.

<script type="text/javascript" src="wi.js">
</script>

<form action="">
  <p>
  <label>
    Enter zip code:
  </label>

  <input name="zip" type = "text">
  <input type="button" onclick="wi(zip.value)"
         value="Get weather information">
</form>

<p>Weather information:</p>

<div>
<div id="wi" style="float: left">
</div>

<div style="clear: both">
</div>
</div>

Listing 7: Client-side HTML for this example

Each time this form's button is pressed, its onclick event handler invokes wi(zip.value) to pass the textfield's contents to this function, which Listing 8 reveals.

function wi(zip)
{
   var xmlHttpReq;

   if (window.XMLHttpRequest)
   {
      // The following is supported by Firefox, Chrome, Opera, Safari, and IE7+.

      xmlHttpReq = new XMLHttpRequest();
   }
   else 
   if (window.ActiveXObject)
   {
      // The following is supported by IE5 and IE6.

      xmlHttpReq = new ActiveXObject("Microsoft.XMLHTTP");
   }
   else
   {
      alert("Your browser doesn't support XMLHttpRequest!");
      return;
   }

   var url = "http://tutortutor.ca/cgi-bin/wi.php";
   url = url+"?zip="+zip;
   xmlHttpReq.onreadystatechange = stateChanged;
   xmlHttpReq.open("GET", url);
   xmlHttpReq.send(null);

   function stateChanged()
   {
      if (xmlHttpReq.readyState == 4)
      {
         var result = "<div style='border: solid; padding: 10px'>";
         result += "<center><strong>";

         var xmlDoc = xmlHttpReq.responseXML;
         var desc = xmlDoc.getElementsByTagName("description")[0]
                          .childNodes[0].nodeValue;
         result += desc+"<br>";

         // If valid ZIP was entered, desc.indexOf() doesn't return -1.

         if (desc.indexOf("Error") == -1)
         {
            // Extract units indicator (C, Celsius, or F, Fahrenheit)

            var units = xmlDoc.getElementsByTagName("yweather:units")[0];
            if (units == null)
               units = xmlDoc.getElementsByTagNameNS("*", "units")[0];
            if (units == null)
            {
                alert("Your browser doesn't properly handle the DOM API's "+
                      "getElementsByTagName() function. Please use a more "+
                      "capable browser (such as Mozilla Firefox 3.5.13) to "+
                      "run this example.");
                return;
            }
            var attr = units.attributes;

            // Extract temperature.

            var temperature = "";
            for (var i = 0; i < attr.length; i++)
               if (attr[i].nodeName == "temperature")
                  temperature = attr[i].nodeValue;

            // Extract weather conditions.

            var cond = xmlDoc.getElementsByTagName("yweather:condition")[0];
            if (cond == null)
               cond = xmlDoc.getElementsByTagNameNS("*", "condition")[0];
            attr = cond.attributes;

            var text = ""; // Sunny, cloudy, etc.
            var code = ""; // Conditions image GIF file ID
            var temp = ""; // Temperature
            var date = ""; // Date to which conditions apply

            for (var i = 0; i < attr.length; i++)
                if (attr[i].nodeName == "text")
                   text = attr[i].nodeValue;
                else
                if (attr[i].nodeName == "code")
                   code = attr[i].nodeValue;
                else
                if (attr[i].nodeName == "temp")
                   temp = attr[i].nodeValue;
                else
                if (attr[i].nodeName == "date")
                   date = attr[i].nodeValue;

            result += "As of: "+date+"<br><br>";

            // Extract conditions image GIF and border it with a table.

            result += "<table border=1 bgcolor=#ffffff><tr><td>";
            result += "<img src='http://l.yimg.com/us.yimg.com/i/us/we/52/"+
                      code+".gif'";
            result += "</td></tr></table><br>";

            // Append current conditions to result.

            result += "Current Conditions: ";
            result += text+", "+temp+" "+temperature;
         }

         result += "</strong></center></div>";
         document.getElementById("wi").innerHTML = result;
      }
   }
}

Listing 8: wi.js

After obtaining the XMLHttpRequest object, wi() builds a URL that identifies the PHP script (see Listing 9) to execute along with its zip argument. It then assigns a function that processes the script's returned XML content to onreadystatechange, invokes open() to identify the GET request method and the script's URL, and invokes send() to launch the script.

<?php

// Get the zip argument from the PHP URL.

$zip = $_GET["zip"];

// Create the Yahoo weather URL for the specified zip.

$url = "http://weather.yahooapis.com/forecastrss?p=".$zip;

// We're sending XML content back to the browser.

header('Content-Type: text/xml');

// Echo the weather XML associated with the specified zip to the browser.

echo file_get_contents($url);
?>

Listing 9: wi.php

When send() returns, wi() exits -- it's reinvoked when the button is pressed. Meanwhile, stateChanged() is asynchronously invoked as described earlier. When this function detects a readyState value of 4, it obtains the returned XML document via responseXML, and uses the DOM API to extract meaningful values from this object.

The parsed content is embedded in the div identified as wi (see Listing 7) via document.getElementById("wi").innerHTML = result;. This div's float: left style causes the div to appear on the left side of the page with its content remaining centered, instead of stretching the div with its centered content across the page.

If you're wondering why I access Yahoo! Weather via wi.php instead of wi.js, recall that XMLHttpRequest requires the target URL to specify the same protocol, host, and port components as the invoking document's URL. This is known as the same origin policy.

The Mozilla team behind Firefox provides a workaround that lets XMLHttpRequest perform cross-site HTTP requests. First introduced into Firefox 3.5, this workaround relies on an Origin HTTP request header and an Access-Control-Allow-Origin response header.

Exercises

  1. What is Ajax?

  2. What is XMLHttpRequest?

  3. For Firefox, Chrome, Opera, Safari, and IE7+, the XMLHttpRequest object is a property of which DOM object?

  4. Identify the parameters of the XMLHttpRequest object's open() function. Which parameters are required and which parameters are optional?

  5. Why do I call getElementsByTagNameNS() in addition to getElementsByTagName()?

  6. Suppose you have a date.php script that outputs the current date. Write a getDate() function that's similar to the earlier countVaC(str) function, but which takes no arguments, uses Ajax to run http://somesite/cgi-bin/date.php, and stores the resulting date text in a span whose id attribute's value is set to date.

Summary

Ajax is a group of interrelated web technologies (JavaScript, XMLHttpRequest, XML, DOM, HTML, CSS) used on the browser side to create interactive web apps. Browsers use Ajax to send data asynchronously (or even synchronously) to servers so that web pages don't need to be reloaded.

Get ZIP

Development Tools

Windows Notepad

Testing Platforms

Mozilla Firefox 3.6.13 on Windows XP SP3 and my ISP's Linux web server with PHP 5.2.13