Explore Java and more with Jeff 'JavaJeff' Friesen

Ajax By Example

Historically, accessing Web applications from Web browsers consisted of inputting content via an HTML form's fields, clicking a submit button to send this content to the server, and receiving (from the server) a new page whose content replaced the form page.

This experience proved to be less elegant than the experience of interacting with desktop programs, which "stay on the same page." Furthermore, the experience felt sluggish due to Web server processing delays, network latency, and other delays between submitting a page and receiving a response. This problem needed a solution.

Ajax, an acronym for Asynchronous JavaScript and XML, was developed as this problem's solution. It uses JavaScript, XML, CSS, XMLHttpRequest, and other standard Web technologies to improve the interaction experience with Web applications.

This article introduces you to Ajax in an example-oriented fashion. Before presenting examples that demonstrate this technology suite, the article explores XMLHttpRequest, which improves browser-based script interaction with server-based Web applications by letting scripts communicate asynchronously with these applications.

XMLHttpRequest

XMLHttpRequest is a Document Object Model (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 application and retrieve application response data for processing and presentation.

The data that is returned from the Web application typically 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 data object for use with the current DOM.

The following steps are required to work with XMLHttpRequest:

  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 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.

  3. Prepare to send an HTTP/HTTPS request to a Web application 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 application via a URL. Three additional 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 should this rule be 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 application by specifying it as this function's solitary argument, but it's common to pass null indicating 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 an activity causes the readyState property to be 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.

If the Web application returns a valid XML document, XMLHttpRequest's responseXML property will contain a DOM document object that provides access to this content via various methods. Also, the responseText property will typically contain the response in plain text, regardless of the content being sent back as an XML document.

Vowels and Consonants

In the first example, you enter characters into a form's text field. For each entered character, a PHP script is invoked to count the number of vowels and consonants in the text that has so far been entered; the resulting metrics are displayed below the form.

Vowels and consonants:

This example relies on Listing 1, which includes a JavaScript source file that defines a function named countVaC(), and 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 the first example

As keys are pressed, this form's text field's onkeyup event handler invokes countVaC(this.value) to pass the text field'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://www.javajeff.mb.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 as described earlier. 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 the second example, you select a month from a form's months dropdown listbox, or a year from the years dropdown listbox. For each selection, a PHP script is invoked to create a calendar for that month/year combination, and the resulting calendar is displayed below these listboxes.

Calendar:

This example relies on Listing 4, which includes a JavaScript source file that defines a function named obtainCal(), and 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 the second example

As listbox selections are made, the form's selected listbox's onchange event handler invokes obtainCal(months.value, years.value) to pass both listbox selections to this function, which is presented 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://www.javajeff.mb.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 the third example, you enter a United States zip code into a text field 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. The document is parsed and weather information is presented.

Weather information:

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

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

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

  <input name="zip" type = "text">
  <input type="button" onclick="gwi(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 the third example

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

function gwi (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://www.javajeff.mb.ca/cgi-bin/gwi.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];
              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];
              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: gwi.js

After obtaining the XMLHttpRequest object, gwi() 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: gwi.php

When send() returns, gwi() 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 gwi.php instead of gwi.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 has provided a workaround that allows XMLHttpRequest to perform cross-site HTTP requests. This solution was first introduced into Firefox 3.5, and makes use of an Origin HTTP request header and an Access-Control-Allow-Origin response header.

Conclusion

If you've never worked with Ajax, you should now possess a basic understanding of how to use this technology suite, and (after playing with this article's examples) how Ajax can make a website more dynamic. From time to time, I'll introduce additional examples that demonstrate more of what Ajax has to offer.


Download code.zip

Note: All code created and tested with Mozilla Firefox 3.5.4, Internet Explorer 8.0.6001.18702, and PHP 4.4.8


LEARN JAVA FROM APRESS