Chapter 8
Ajax

It’s probably not an understatement to say that Ajax has revitalized the Web. It’s certainly one of the reasons for a resurgent interest in JavaScript and it might even be the reason that you’re reading this book.

Irrespective of the hyperbole surrounding Ajax, the technology has dramatically affected the way in which people can interact with a web page. The ability to update individual parts of a page with information from a remote server mightn’t sound like a revolutionary change, but it has facilitated a seamless type of interaction that has been missing from HTML documents since their inception.

This ability to create a fluidly updating web page has captured the imaginations of developers and interaction designers alike; they’ve flocked to Ajax in droves, using it to create the next generation of web applications, as well as annoy the heck out of the average user. As with any new technology, there’s a temptation to overuse Ajax; but when it’s used sensibly, it can definitely create more helpful, more responsive, and more enjoyable interfaces for users to explore.

Although quite a few of the JavaScript libraries out there will offer you a complete “Ajax experience” in a box, there is really no substitute for the freedom that comes with knowing how it works from the ground up. So let’s dive in!

XMLHttpRequest: Chewing Bite-sized Chunks of Content

The main concept of Ajax is that you’re instructing the browser to fetch small pieces of content instead of big ones; instead of a page you might request a single paragraph.

Although cross-browser Ajax-type functionality was hacked together previously with iframes, the current Ajax movement was sparked when XMLHttpRequest became available in more than just Internet Explorer.

XMLHttpRequest is a browser feature that allows JavaScript to make a call to a server without going through the normal browser page-request mechanism. This means that JavaScript can make additional server requests behind the scenes while a page is being viewed. In effect, this allows us to pull down extra data from the server, then manipulate the page using the DOM—replacing sections, adding sections, or deleting sections depending on the data we receive. The distinction between normal and Ajax requests is illustrated in Figure 8.1.

Comparing a normal page request (replacing the whole page) with an Ajax request (replacing part of the page) normal page request Ajax request

Figure 8.1. Comparing a normal page request (replacing the whole page) with an Ajax request (replacing part of the page)

Communications with the server that don’t use the page-request mechanism are called asynchronous requests, because they can be made without interrupting the user’s page interaction. A normal page request is synchronous, in that the browser waits for a response from the server before any more interaction is allowed.

XMLHttpRequest is really the only aspect of Ajax that’s truly new. Every other part of an Ajax interaction—the event listener that triggers it, the DOM manipulation that updates the page, and so on—has been covered in previous chapters of this book already. So, once you know how to make an asynchronous request, you’re ready to go.

Creating an XMLHttpRequest Object

Internet Explorer 5 and 6 were the first browsers to implement XMLHttpRequest, and they did so using an ActiveX object:[29]

var requester = new ActiveXObject("Microsoft.XMLHTTP");

Every other browser that supports XMLHttpRequest (including Internet Explorer 7) does so without using ActiveX. A request object for these browsers looks like this:

var requester = new XMLHttpRequest();

Warning: ActiveX is Unreliable

The way that XMLHttpRequest is implemented in Internet Explorer 6 and earlier means that if a user has disabled trusted ActiveX controls, XMLHttpRequest will be unavailable to you even if JavaScript is enabled. Many people disable untrusted ActiveX controls, but disabling trusted ActiveX controls is less common.

We can easily reconcile the differences between the two methods of object creation using a try-catch statement, which will automatically detect the correct way to create an XMLHttpRequest object:

try-catch_test.js (excerpt)
try
{
  var requester = new XMLHttpRequest();(1)
}
catch (error)(2)
{
  try
  {
    var requester = new ActiveXObject("Microsoft.XMLHTTP");(3)
  }
  catch (error)
  {
    var requester = null;
  }
}

A try statement allows you to try out a block of code, but if anything inside that block causes an error, the program won’t stop execution entirely; instead, it moves onto the catch statement and runs the code inside that. As you can see in Figure 8.2, the whole structure is like an if-else statement, except the branch taken is conditional on any errors occurring.

The logical structure or a try-catch statement try-catch statement logical structure

Figure 8.2. The logical structure or a try-catch statement

We need to use a try-catch statement to create ActiveX objects because an object detection test will indicate that ActiveX controls are still available even if a user has disabled them (though your script will throw an error when you actually try to create an ActiveX object).

Note: The Damage Done

If an error occurs inside a try statement, the program will not revert to the state it had before the try statement was executed—instead, it will switch immediately to the catch statement. Thus, any variables that were created before the error occurred will still exist. However, if an error occurs while a variable is being assigned, that variable will not be created at all.

Here’s what happens in the code above:

(1)

We try to create an XMLHttpRequest object using the cross-browser method. If our attempt is successful, the variable requester will be a new XMLHttpRequest object. But if XMLHttpRequest is unavailable, the code will cause an error. We can try out a different method inside the catch statement.

(2)

catch statements “catch” the exception that caused the try statement to fail. This exception is identified by the variable name that appears in brackets after catch, and it’s mandatory to give that exception a name (even if we’re not going to use it). You can give the exception any name you like, but I think error is nicely descriptive.

(3)

Inside the catch, we try to create an XMLHttpRequest object via ActiveX. If that attempt is successful, requester will be a valid XMLHttpRequest object, but if we still can’t create the object, the second catch statement sets requester to null. This makes it easy to test whether the current user agent supports XMLHttpRequest, and to fork to some non-Ajax fallback code (such as that which submits a form normally):

if (requester == null)
{
  code for non-Ajax clients
}
else
{
  code for Ajax-enabled clients
}

Thankfully, the significant differences between browser implementations of XMLHttpRequest end with its creation. All of the basic data communication methods can be called using the same syntax, irrespective of the browser in which they’re running.

Calling a Server

Once we’ve created an XMLHttpRequest object, we must call two separate methods—open and send—in order to get it to retrieve data from a server.

open initializes the connection and takes two required arguments, with several optionals. The first argument is the type of HTTP request you want to send (GET, POST, DELETE, etc.); the second is the location from which you want to request data. For instance, if we wanted to use a GET request to access feed.xml in the root directory of a web site, we’d initialize the XMLHttpRequest object like this:

requester.open("GET", "/feed.xml", true);

The URL can be either relative or absolute, but due to cross-domain security concerns the target must reside on the same domain as the page that’s requesting it.

Note: HTTP Only

Quite a few browsers will only allow XMLHttpRequest calls via http:// and https:// URLs, so if you’re viewing your site locally via a URL beginning with file://, your XMLHttpRequest call may not be allowed.

The third argument of open is a Boolean that specifies whether the request is made asynchronously (true) or synchronously (false). A synchronous request will freeze the browser until the request has completed, disallowing user interaction in the interim. An asynchronous request occurs in the application’s background, allowing other scripts to run and the user to access the browser at the same time. I recommend you use asynchronous requests; otherwise, you run the risk of users’ browsers locking up while they wait for a request that has gone awry. open also has optional fourth and fifth arguments that specify the user’s name and password for authentication purposes when a password-protected URL is requested.

Once open has been used to initialize a connection, the send method activates that connection and makes the request. send takes one argument that allows you to send encoded data along with a POST request, in the same format as a form submission:

requester.open("POST", "/query.php", true);
requester.setRequestHeader("Content-Type",
    "application/x-www-form-urlencoded");
requester.send("name=Clark&[email protected]");

Note: Content-Type Required

Opera requires you to set the Content-Type header of a POST request using the setRequestHeader method. Other browsers don’t require it, but it’s the safest approach to take to allow for all browsers. Any use of the setRequestHeader method must occur after the open method call, but before the send method call.

To simulate a form submission using a GET request, you need to hard-code the names and values into the open URL, then execute send with a null value:

requester.open("GET",
    "query.php?name=Clark&[email protected]", true);
requester.send(null);

Internet Explorer doesn’t require you to pass any value to send, but Mozilla browsers will return an error if no value is passed; that’s why null is included in the above code.

Once you’ve called send, XMLHttpRequest will contact the server and retrieve the data that you requested. In the case of an asynchronous request, the function that created the connection will likely finish executing while the retrieval takes place. In terms of program flow, making an XMLHttpRequest call is a lot like setTimeout, which you’ll remember from Chapter 5.

We use an event handler to notify us that the server has returned a response. In this particular case, we’ll need to handle changes in the value of the XMLHttpRequest object’s readyState property, which specifies the status of the object’s connection, and can take any of these values:

0

uninitialized

1

loading

2

loaded

3

interactive

4

complete

We can monitor changes in the readyState property by handling readystatechange events, which are triggered each time the property’s value changes:

requester.onreadystatechange = readystatechangeHandler;

function readystatechangeHandler()
{
  code to handle changes in XMLHttpRequest readystate
}

readyState increments from 0 to 4, and the readystatechange event is triggered for each increment. However, we only really want to know when the connection has completed (that is, readyState equals 4), so our handling function needs to check for this value.

Upon the connection’s completion, we also have to check whether the XMLHttpRequest object successfully retrieved the data, or was given an HTTP error code such as 404 (page not found). You can determine this from the request object’s status property, which contains an integer value. A value of 200 is a fulfilled request; you should check for it—along with 304 (not modified)—as these values indicate successfully retrieved data. However, status can take as a value any of the HTTP codes that servers are able to return, so you may want to write some conditions that will handle some other codes. In general, however, you’ll need to specify a course of action for your program to take if the request is not successful:

requester.onreadystatechange = readystatechangeHandler;

function readystatechangeHandler()
{
  if (requester.readyState == 4)
  {
    if (requester.status == 200 || requester.status == 304)
    {
      code to handle successful request
    }
    else
    {
      code to handle failed request
    }
  }
}

Instead of assigning a function that’s defined elsewhere as the readystatechange event handler, you can declare a new, anonymous (unnamed) function inline:

requester.onreadystatechange = function()
{
  if (requester.readyState == 4)
  {
    if (requester.status == 200 || requester.status == 304)
    {
      code to handle successful request
    }
    else
    {
      code to handle failed request
    }
  }
}

The advantage of specifying the readystatechange callback function inline like this is that the requester object will be available inside that function via a closure. If the readystatechange handler function is declared separately, you’ll need to jump through hoops to obtain a reference to the requester object inside the handling function.

Note: XMLHttpRequest is Non-recyclable

Even though an XMLHttpRequest object allows you to call the open method multiple times, each object can effectively only be used for one call, as the readystatechange event refuses to fire again once readyState changes to 4 (in Mozilla browsers). Therefore, you will have to create a new XMLHttpRequest object every time you want to retrieve new data from the server.

Dealing with Data

If you’ve made a successful request, the next logical step is to read the server’s response. Two properties of the XMLHttpRequest object can be used for this purpose:

responseXML

This property stores a DOM tree representing the retrieved data, but only if the server indicated a content-type of text/xml for the response. This DOM tree can be explored and modified using the standard JavaScript DOM access methods and properties we explored in Chapter 3, such as getElementsByTagName, childNodes, and parentNode.

responseText

This property stores the response data as a single string. If the content-type of the data supplied by the server was text/plain or text/html, this is the only property that will contain data. In the case of a text/xml response, this property will also contain the XML code as a text string, providing an alternative to responseXML.

In simple cases, plain text works perfectly well as a means of transmitting and handling the response, so the XMLHttpRequest object doesn’t exactly live up to its name. When we’re dealing with more complex data structures, however, XML can provide a convenient way to express those structures:

<?xml version="1.0" ?>
<user>
  <name>Doctor Who</name>
  <email>[email protected]</email>
</user>
<user>
  <name>The Master</name>
  <email>[email protected]</email>
</user>

responseXML allows us to access different parts of the data using all the DOM features with which we’re familiar from our dealings with HTML documents. Remember that data contained between tags is considered to be a text node inside the element in question. With that in mind, extracting a single value from a responseXML structure is reasonably easy:

var nameNode =
    requester.responseXML.getElementsByTagName("name")[0];
var nameTextNode = nameNode.childNodes[0];
var name = nameTextNode.nodeValue;

The name variable will now take as its value the first user’s name: "Doctor Who".

Note: Whitespace Generates Text Nodes

As in HTML documents, be aware that whitespace between tags in XML will often be interpreted as a text node. If in doubt, remember that you can check if a given node is an element by looking at its nodeType property, as described in Chapter 4.

We can use the data contained in the XML to modify or create new HTML content, updating the interface on the fly in the manner that has become synonymous with Ajax.

The main downside of using XML with JavaScript is that a fair amount of work can be involved in parsing XML structures and accessing the information we want. However, an alternative way to use the XMLHttpRequest object is to remove this data processing layer and allow the server to return HTML code, all ready to insert into your page. This approach is taken by libraries such as Prototype, in which HTML is delivered with a MIME type of text/html and the value of responseText is automatically inserted into the document using innerHTML, overwriting the contents of an existing element.

As with any use of innerHTML, this technique suffers from the disadvantages we discussed in Chapter 4, but it can certainly be a viable option if your circumstances require it.

A Word on Screen Readers

Until now, we’ve always taken time to make sure our JavaScript enhancements do not prevent users of assistive technologies—like screen readers—from using our sites. Unfortunately, when you add Ajax to the equation, this goal becomes extremely difficult, if not impossible to achieve for screen reader users in particular.

Most, if not all of the current screen readers are unable to handle in a sensible (let alone useful) way the on-the-fly page updates that typify Ajax development. Screen readers either will not pick up those changes at all, or they’ll pick them up at the most untimely of moments.

In some very specific cases, developers have begun to produce experimental solutions that start to address these issues, but we’re a long way from having reliable, best-practice techniques in hand. The prevailing wisdom suggests that any real solution will have to be developed at least in part by the screen reader vendors themselves.

This leaves web developers like you and me with a tough decision: do we abandon Ajax and the amazing usability enhancements that it makes possible, or do we shut out screen reader users and take full advantage of Ajax? Of course, if you can justify asking screen reader users to disable JavaScript when visiting your site, you can offer these users the same fallback experience as other non-JavaScript users, which can work just fine. But you’ll need to make sure these users can find out enough about your site to decide if it’s worth disabling JavaScript to proceed. And consider making it even easier—a link that said “Disable user interface features on this site that are not compatible with screen readers,” for example, would not be out of the question.

Putting Ajax into Action

Now you know the basics of Ajax—how to create and use an XMLHttpRequest object. But it’s probably easier to understand how Ajax fits into a JavaScript program if you try out a simple example.

In this example, we’ll retrieve information that’s relevant to the selections users make from the widget shown in Figure 8.3. This tool allows users to choose any of three cities; each selection will update the widget’s display with the weather for that location.

In our weather widget, the user is asked to select a particular city

Figure 8.3. In our weather widget, the user is asked to select a particular city

The HTML for the widget looks like this:

weather_widget.html (excerpt)
<div id="weatherWidget">
  <h2>Weather</h2>
  <p>Please select a city:</p>
  <ul>
    <li>
      <a href="/weather/london/">London</a>
    </li>
    <li>
      <a href="/weather/new_york/">New York</a>
    </li>
    <li>
      <a href="/weather/melbourne/">Melbourne</a>
    </li>
  </ul>
</div>

We’ll override those anchors with our Ajax code, but it’s important to note that the href attribute of each anchor points to a valid location. This means that users who have JavaScript or XMLHttpRequest turned off will still be able to get the information; they just won’t be gobsmacked by our cool use of Ajax to retrieve it.

When creating Ajax functionality, you can generally follow this pattern:

  1. Initialize event listeners.

  2. Handle event triggers.

  3. Create an XMLHttpRequest connection.

  4. Parse data.

  5. Modify the page.

To handle the behavior of this weather widget, we’ll create a WeatherWidget object. Its initialization function will start by adding event listeners to those anchor tags, which will capture any clicks that the user makes:

weather_widget.js (excerpt)
var WeatherWidget =
{
  init: function()
  {
    var weatherWidget = document.getElementById("weatherWidget");
    var anchors = weatherWidget.getElementsByTagName("a");

    for (var i = 0; i < anchors.length; i++)
    {
      Core.addEventListener(anchors[i], "click",
          WeatherWidget.clickListener);
    }
  },
  …
};

Each of those anchors now has a click event listener, but what happens when the event is fired? Let’s fill out the listener method, clickListener:

clickListener: function(event)
{
  try
  {
    var requester = new XMLHttpRequest();(1)
  }
  catch (error)
  {
    try
    {
      var requester = new ActiveXObject("Microsoft.XMLHTTP");
    }
    catch (error)
    {
      var requester = null;
    }
  }

  if (requester != null)(2)
  {
    var widgetLink = this;
    widgetLink._timer = setTimeout(function()(3)
        {
          requester.abort();

          WeatherWidget.writeError(
              "The server timed out while making your request.");
        }, 10000);

    var city = this.firstChild.nodeValue;(4)

    requester.open("GET", "ajax_weather.php?city=" +
        encodeURIComponent(city), true);(5)
    requester.onreadystatechange = function()(6)
    {
      if (requester.readyState == 4)
      {
        clearTimeout(widgetLink._timer);

        if (requester.status == 200 || requester.status == 304)
        {
          WeatherWidget.writeUpdate(requester.responseXML);
        }
        else
        {
          WeatherWidget.writeError(
              "The server was unable to be contacted.");
        }
      }
    };
    requester.send(null);(7)

    Core.preventDefault(event);(8)
  }
}

(1)

The start of this function is occupied by the standard XMLHttpRequest creation code that we looked at earlier in this chapter.

(2)

Once that has been executed, the real logic of clickListener is contained inside the conditional statement if (requester != null). By using this condition, we ensure that the Ajax code is only executed if the XMLHttpRequest object is available. Otherwise, the event handling function will exit normally, allowing the browser to navigate to the href location, just as it would if JavaScript wasn’t enabled. This approach provides an accessible alternative for users without Ajax capability.

(3)

This is a real-world Ajax application that’s subject to the unreliability of network traffic, so before the actual Ajax call is made, it’s a good idea to place a time limit on the transaction to ensure that the user won’t be sitting around waiting forever if the server fails to respond. To establish this limit, we assign a setTimeout call as a custom property of the link that was clicked (widgetLink), with a ten-second delay. If the Ajax request takes longer than ten seconds, the function supplied to setTimeout will be called, canceling the request via the abort method, and supplying the user with a sensible error message. Just like the readystatechange listener, the timeout function is specified inline, so requester will be available via a closure. We’ll have to remember to stop this timeout later, if and when the Ajax request is actually completed.

(4)

In the first line of code after the timeout function, we determine which city was selected. To do so, we get the value of the text from the anchor that was clicked. We assume that the link contains a text node—so that will be the first child node of the anchor—and the city name itself will be the nodeValue of that text node. We can pass this value to our server-side script in order to access the weather data for that particular city.

(5)

Once the requested city has been identified, we begin our Ajax connection. For this example we’re using a GET request, and the server-side script we’re trying to access is at ajax_weather.php. Since we’re using GET, the request variables have to be encoded directly into the URL. To do so, we append a question mark (?) to the script location, followed by the variables specified as name=value pairs. If you want to pass multiple variables (we don’t in this case), each pair must be separated by an ampersand (&).

In this example, we’re passing the city as a request variable called city; its value is the city name we extracted from the link. The actual value is generated by the built-in escapeURIComponent function, which will encode values so that they don’t cause errors when being used as part of a URL. You should encode any string values that you attach to a URL this way.

(6)

Once the requester object has been initialized with requester.open, we set up our readystatechange event handler as an anonymous inline function. The code inside this handler is almost identical to the template that we outlined earlier in this chapter, except that we have filled in the actions that are to be taken for successful and unsuccessful requests. You should also note that once a response has been received from the server, a clearTimeout call is executed to cancel the setTimeout call we made earlier. This ensures that the user won’t receive an error message about the server timing out when it hasn’t actually done so.

(7)

Directly after the onreadystatechange function declaration, we fire off the requester’s send method. The Ajax call is now in action.

(8)

We don’t want any clicks on our Ajax links to take the user to a new page (that would defeat the links’ purposes!), so the last line of clickListener stops the browser from performing the link’s default action.

What actually occurs on the server after we make our Ajax request isn’t the concern of our JavaScript. In our code, we’ve referred to a PHP script that will return information on the basis of the value of the city we passed it, but we could equally refer to a JSP script, a Ruby on Rails action, or a .NET controller. Whatever technology is used, we simply need it to return some correctly formatted XML.

If a successful request occurred, we call our writeUpdate method, and pass it the responseXML data returned by the server. If there was an unsuccessful request, we call writeError and give it a suitable error message.

When writeUpdate is called, we know that we’ve got some XML data waiting to be parsed and added to our HTML. In order to use it, we need to extract particular data points from the XML, then insert them into appropriate elements in our page.

When you’re liaising with a custom server-side script, you’ll have to agree on a format for the XML, so that the server-side script can write it correctly and the JavaScript can read it correctly. For this example, we’re going to assume that the XML has a form like this:

melbourne.xml
<?xml version="1.0" ?>
<city>
  <name>Melbourne</name>
  <temperature>18</temperature>
  <description>Fine, partly cloudy</description>
  <description_class>partlyCloudy</description_class>
</city>

Knowing this structure makes it easy for us to write writeUpdate to extract the pertinent data:

weather_widget.js (excerpt)
writeUpdate: function(responseXML)
{
  var nameNode = responseXML.getElementsByTagName("name")[0];(1)
  var nameTextNode = nameNode.firstChild;
  var name = nameTextNode.nodeValue;

  var temperatureNode =
      responseXML.getElementsByTagName("temperature")[0];
  var temperatureTextNode = temperatureNode.firstChild;
  var temperature = temperatureTextNode.nodeValue;

  var descriptionNode =
      responseXML.getElementsByTagName("description")[0];
  var descriptionTextNode = descriptionNode.firstChild;
  var description = descriptionTextNode.nodeValue;

  var descriptionClassNode =
      responseXML.getElementsByTagName("description_class")[0];
  var descriptionClassTextNode = descriptionClassNode.firstChild;
  var descriptionClass = descriptionClassTextNode.nodeValue;

  var weatherWidget = document.getElementById("weatherWidget");(2)
  while (weatherWidget.hasChildNodes())
  {
    weatherWidget.removeChild(weatherWidget.firstChild);
  }

  var h2 = document.createElement("h2");(3)
  h2.appendChild(document.createTextNode(name + " Weather"));
  weatherWidget.appendChild(h2);

  var div = document.createElement("div");
  div.setAttribute("id", "forecast");
  div.className = descriptionClass;
  weatherWidget.appendChild(div);

  var paragraph = document.createElement("p");
  paragraph.setAttribute("id", "temperature");
  paragraph.appendChild(
      document.createTextNode(temperature + "u00B0C"));(4)
  div.appendChild(paragraph);

  var paragraph2 = document.createElement("p");
  paragraph2.appendChild(document.createTextNode(description));
  div.appendChild(paragraph2);
}

(1)

The first four paragraphs of code inside writeUpdate parse the XML to get a particular tag value. As you can see, parsing XML can be quite tedious, but because we know the syntax of the data, we can go directly to the elements we need and extract their values fairly easily.

(2)

Right after we’ve finished parsing the data, we get a reference to the widget container and clean out its contents by removing all of its child nodes. This gives us an empty element into which we can insert our new data.

(3)

Using this clean slate, we can go about creating the new HTML elements that are going to represent the weather report. We’ll use the data from the XML to create the relevant content.

(4)

If you have a sharp eye, you’ll have noticed the peculiar text that we specified for the contents of paragraph. What the heck does "u00B0C" mean, anyway?

In fact, that will be displayed as °C (as in “It’s a lovely 20°C outside”). The code u00B0 is a JavaScript character code for Unicode character number 00B0, which is the degree symbol (°).

In an HTML document, you could just type the ° character verbatim, assuming you’re hip to the whole Unicode thing and your HTML is written in UTF-8, or you could use the HTML character entity &deg;. JavaScript strings, on the other hand, only support Latin-1 (ISO-8859-1) characters in older browsers, and they don’t support HTML character entities at all.

So whenever you need to include in a JavaScript string a character that you can’t easily type on an English keyboard (which means it may not be within the Latin-1 character set), your best bet is to look up its Unicode character number (using a tool like Character Map, which is built into Windows, or Mac OS X’s Character Palette) and replace it with a uXXXX code.

After we finish manipulating the DOM, the HTML of the weather widget will look roughly like this:

<div id="weatherWidget">
  <h1>
    Melbourne Weather
  </h1>
  <div id="forecast" class="partlyCloudy">
    <p id="temperature">
      18°C
    </p>
    <p>
      Fine, partly cloudy
    </p>
  </div>
</div>

The class on the forecast element enables us to style the weather forecast with a little icon, producing an updated widget that looks like Figure 8.4.

The weather widget with its updated content weather widget updated content

Figure 8.4. The weather widget with its updated content

Our little Ajax program is almost finished. All that’s left is to handle the error that will be returned if our server doesn’t return proper data:

weather_widget.js (excerpt)
  writeError: function(errorMsg)
  {
    alert(errorMsg);
  }
};

That one’s really simple: the error message supplied to writeError is popped up in an alert box, letting the user know that something went wrong.

With the methods written, all we have to do is throw them together into one object, and initialize it with Core.start:

weather_widget.js
var WeatherWidget =
{
  init: function()
  {
    var weatherWidget = document.getElementById("weatherWidget");
    var anchors = weatherWidget.getElementsByTagName("a");

    for (var i = 0; i < anchors.length; i++)
    {
      Core.addEventListener(anchors[i], "click",
          WeatherWidget.clickListener);
    }
  },
  clickListener: function(event)
  {
    try
    {
      var requester = new XMLHttpRequest();
    }
    catch (error)
    {
      try
      {
        var requester = new ActiveXObject("Microsoft.XMLHTTP");
      }
      catch (error)
      {
        var requester = null;
      }
    }

    if (requester != null)
    {
      var widgetLink = this;
      widgetLink._timer = setTimeout(function()
          {
            requester.abort();

            WeatherWidget.writeError(
                "The server timed out while making your request.");
          }, 10000);

      var city = this.firstChild.nodeValue;

      requester.open("GET", "ajax_weather.php?city=" +
          encodeURIComponent(city), true);
      requester.onreadystatechange = function()
      {
        if (requester.readyState == 4)
        {
          clearTimeout(widgetLink._timer);

          if (requester.status == 200 || requester.status == 304)
          {
            WeatherWidget.writeUpdate(requester.responseXML);
          }
          else
          {
            WeatherWidget.writeError(
                "The server was unable to be contacted.");
          }
        }
      };
      requester.send(null);

      Core.preventDefault(event);
    }
  },
  writeUpdate: function(responseXML)
  {
    var nameNode = responseXML.getElementsByTagName("name")[0];
    var nameTextNode = nameNode.firstChild;
    var name = nameTextNode.nodeValue;

    var temperatureNode =
        responseXML.getElementsByTagName("temperature")[0];
    var temperatureTextNode = temperatureNode.firstChild;
    var temperature = temperatureTextNode.nodeValue;

    var descriptionNode =
        responseXML.getElementsByTagName("description")[0];
    var descriptionTextNode = descriptionNode.firstChild;
    var description = descriptionTextNode.nodeValue;

    var descriptionClassNode =
        responseXML.getElementsByTagName("description_class")[0];
    var descriptionClassTextNode = descriptionClassNode.firstChild;
    var descriptionClass = descriptionClassTextNode.nodeValue;

    var weatherWidget = document.getElementById("weatherWidget");
    while (weatherWidget.hasChildNodes())
    {
      weatherWidget.removeChild(weatherWidget.firstChild);
    }

    var h2 = document.createElement("h2");
    h2.appendChild(document.createTextNode(name + " Weather"));
    weatherWidget.appendChild(h2);

    var div = document.createElement("div");
    div.setAttribute("id", "forecast");
    div.className = descriptionClass;
    weatherWidget.appendChild(div);

    var paragraph = document.createElement("p");
    paragraph.setAttribute("id", "temperature");
    paragraph.appendChild(
        document.createTextNode(temperature + "u00B0C"));
    div.appendChild(paragraph);

    var paragraph2 = document.createElement("p");
    paragraph2.appendChild(document.createTextNode(description));
    div.appendChild(paragraph2);
  },
  writeError: function(errorMsg)
  {
    alert(errorMsg);
  }
};

Core.start(WeatherWidget);

And there you have it: a little Ajax weather widget that you could pop into the sidebar of one of your sites to let users instantly check the weather without leaving your page. Ajax offers endless possibilities; all you have to do is remember the pattern I described at the start of this example, and you’ll have even the most complex interactions within your reach.

Seamless Form Submission with Ajax

As we saw in Chapter 6, forms are integral to the user experience provided by most web sites. One of the things Ajax allows us to do is to streamline the form submission process by transmitting the contents of a form to the server without having to load an entirely new page into the browser.

It’s fairly simple to extend the Ajax code that we used in the previous example so that it can submit a form. Consider the contact form pictured in Figure 8.5, which uses this code:

The example form to which we’ll apply Ajax submission techniques

Figure 8.5. The example form to which we’ll apply Ajax submission techniques

contact_form.html (excerpt)
<form id="contactForm" action="form_mailer.php" method="POST">
  <fieldset>
    <legend>
      Contact Form
    </legend>
    <label for="contactName">
      Name
    </label>
    <input id="contactName" name="contactName" type="text" />
    <label for="contactEmail">
      Email Address
    </label>
    <input id="contactEmail" name="contactEmail" type="text" />
    <label for="contactType">
      Message Type
    </label>
    <select id="contactType" name="contactType">
      <option value="1">Enquiry</option>
      <option value="2">Spam</option>
      <option value="3">Wedding proposal</option>
    </select>
    <label for="contactMessage">
      Message
    </label>
    <textarea id="contactMessage" name="contactMessage"></textarea>
    <input id="contactNewsletter" name="contactNewsletter"
        type="checkbox" value="1" />
    <label for="contactNewsletter">
      I'd like to receive your hourly newsletter
    </label>
    <fieldset>
      <legend>
        Reply by
      </legend>
      <input id="contactMethodA" name="contactMethod" type="radio"
          value="1" />
      <label for="contactMethodA">
        Email
      </label>
      <input id="contactMethodB" name="contactMethod" type="radio"
          value="2" />
      <label for="contactMethodB">
        Pony messenger
      </label>
    </fieldset>
    <input type="hidden" name="id" value="SS56789" />
    <input type="submit" value="submit" />
  </fieldset>
</form>

In order to submit the form’s contents using Ajax, we need to do a couple of things:

  1. Override the default form submission behavior.

  2. Get the form data.

  3. Submit the form data to the server.

  4. Check for the success or failure of submission.

We’ll create this functionality inside an object called ContactForm. The only thing we need to do when we initialize the object is to override the form’s default submission action. This can easily be done by intercepting the form’s submit event with an event listener:

contact_form.js (excerpt)
var ContactForm =
{
  init: function()
  {
    var contactForm = document.getElementById("contactForm");
    Core.addEventListener(contactForm, "submit",
        ContactForm.submitListener);
  },

Warning: Ajax and Form Validation

Adding multiple event listeners to a given element for a given event can be risky business, because you have no control over the order in which they will be invoked. Thus, it isn’t safe to assign an Ajax form submitter and a client-side form validator separately. If you do so, the submitter may be executed before the validator, and you might end up sending invalid data to the server. Link your validator and your submitter together to ensure that validation takes place before the form is submitted.

Now, before the form is submitted, the submitListener method will be run. It’s inside this function that we collect the form data, send off an Ajax request, and cancel the normal form submission:

contact_form.js (excerpt)
submitListener: function(event)
{
  var form = this;

  try
  {
    var requester = new XMLHttpRequest();
  }
  catch (error)
  {
    try
    {
      var requester = new ActiveXObject("Microsoft.XMLHTTP");
    }
    catch (error)
    {
      var requester = null;
    }
  }

  if (requester != null)
  {
    form._timer = setTimeout(function()
        {
          requester.abort();

          ContactForm.writeError(
              "The server timed out while making your request.");
        }, 10000);

    var parameters = "submitby=ajax";(1)
    var formElements = [];(2)

    var textareas = form.getElementsByTagName("textarea");

    for (var i = 0; i < textareas.length; i++)
    {
      formElements[formElements.length] = textareas[i];(3)
    }

    var selects = form.getElementsByTagName("select");

    for (var i = 0; i < selects.length; i++)
    {
      formElements[formElements.length] = selects[i];
    }

    var inputs = form.getElementsByTagName("input");

    for (var i = 0; i < inputs.length; i++)
    {
      var inputType = inputs[i].getAttribute("type");

      if (inputType == null || inputType == "text" ||
          inputType == "hidden" ||
          (typeof inputs[i].checked != "undefined" &&
          inputs[i].checked == true))(4)
      {
        formElements[formElements.length] = inputs[i];
      }
    }

    for (var i = 0; i < formElements.length; i++)(5)
    {
      var elementName = formElements[i].getAttribute("name");

      if (elementName != null && elementName != "")(6)
      {
        parameters += "&" + elementName + "=" +
            encodeURIComponent(formElements[i].value);(7)
      }
    }

    requester.open("POST", form.getAttribute("action"), true);(8)
    requester.setRequestHeader("Content-Type",
        "application/x-www-form-urlencoded");(9)
    requester.onreadystatechange = function()
    {
      clearTimeout(form._timer);

      if (requester.readyState == 4)
      {
        if (requester.status == 200 || requester.status == 304)
        {
          ContactForm.writeSuccess(form);(10)
        }
        else
        {
          ContactForm.writeError(
              "The server was unable to be contacted.");(11)
        }
      }
    };
    requester.send(parameters);(12)

    Core.preventDefault(event);(13)
  }
},

(1)

One of the most important parts of the submitForm method is the variable parameters. This variable is used to store the serialized contents of the form—all of the field names and values combined into one long string that’s suitable for sending in a POST request. We start this string off with the value "submitby=ajax", which effectively adds to the form a variable named submitby with a value of ajax. The server can use this variable to identify form submissions that are transmitted via Ajax—as opposed to a standard form submission—and respond differently to them (for example, by sending the response in XML format rather than as a full HTML page).

(2)

A form can contain a number of different elements, and we have to deal with all of them in order to produce a properly serialized version of the form’s contents. There are three essential element types—input, select, and textarea—but input elements can have different types with different behaviors, so we have to cater for those as well. In order to minimize code repetition, we use the DOM to get node lists of each of the three element types, then we combine them into one big array—formElements—through which we can iterate in one fell swoop.

(3)

All of the textareas and selects can be added to formElements straight away, because those elements are always of the same type.

(4)

When it comes to input elements, we have to distinguish between text inputs, hidden inputs, checkboxes, and radio buttons. Text inputs and hidden inputs can always be submitted with the form, because they don’t have an on/off toggle. However, we need to test whether checkboxes and radio buttons are checked or not before we add them to the list of submitted elements. We don’t want to submit a value for a checkbox that wasn’t checked, or for the wrong radio button in a group.

Inside the for loop that iterates over the inputs node list, we use a combination of each input’s type and its checked property to determine whether it should be added to formElements. The if statement uses a number of OR conditions to perform this check, and the logic reads like this:

  1. IF the type of the input is null (it will be a text input by default)

  2. OR the type of the input is text

  3. OR the type of the input is hidden

  4. OR the input’s checked property exists AND it is true

  5. THEN add the input to formElements

The fourth point above catches both checkboxes and radio buttons. Any checkbox that’s checked should have its value submitted, and only one radio button in a radio button group will ever have checked set to true, so it’s safe to submit that one as well.

(5)

Once all the valid elements have been added to formElements, we have to write out their name/value pairs in a serialized fashion. This process is identical for all form element types, which is why we can minimize code repetition by building the formElements array in advance.

(6)

As we add each form element to the serialized string, we have to check whether a name is assigned to it.

(7)

If it does have a name, we’ll want to send its value to the server, so we take the name, followed by "=", followed by the value, and add the whole thing to the end of parameters. Each of the name/value pairs in parameters is separated by an ampersand ("&").

You’ll notice that, again, we encode the values of our form elements using encodeURIComponent, to make sure that the request data remains valid no matter which special characters the user types into the form.

We’re now ready to submit our serialized form data string to the server. The XMLHttpRequest code should be pretty familiar to you by now:

The message that appears when the form has been submitted successfully

Figure 8.6. The message that appears when the form has been submitted successfully

(8)

This time, when we open our XMLHttpRequest object, we’ll use the "POST" method. The URL for the server request is pulled directly from the action of the form itself, which increases the reusability of this script.

(9)

Remember that we have to set the content-type in the header of a POST request so that the request will work in Opera.

(10)

In the readystatechange handler, all we’re doing is waiting for a success code from the server. We don’t actually care what data it gives us, so long as we know that it received the contact information. Once we’ve received that confirmation (in the form of a successful status code), we can execute ContactForm.writeSuccess, which will let the user know that the action was successful:

contact_form.js (excerpt)
writeSuccess: function(form)
{
  var newP = document.createElement("p");
  newP.setAttribute("id", "success");
  newP.appendChild(document.createTextNode(
      "Your message was submitted successfully."));
  form.parentNode.replaceChild(newP, form);
},

For this example, the success handler replaces the contact form with a paragraph that reads “Your message was submitted successfully,” as shown in Figure 8.6.

(11)

Similarly, if the server does not successfully receive the contact information, ContactForm.writeError can handle the error in any manner it chooses—with a simple alert telling the user to try again, an error message, or even by shaking the browser window violently from side to side—whatever suits your application best. For reasons of simplicity, and because it’s highly visible to the user, this example uses an alert box:

contact_form.js (excerpt)
writeError: function(errorMsg)
{
  alert(errorMsg);
}

(12)

With that last method in place, the readystatechange handler is ready to do its work, and submitListener is free to fire off the Ajax request. Since this is a POST request, we pass parameters to the send method, rather than including them in the URL itself (as we would for a GET request).

(13)

The very last command in submitListener is a Core.preventDefault call, which stops the browser from submitting the form (as we just did it ourselves via Ajax). And that’s why we call this program “seamless form submission.”

We’ve generalized the code that handles form elements, which means that it’s really easy to take this code and use it in other applications you might work on. Just modify the init method to reference the correct form, and away you go!

Exploring Libraries

Obviously, in this age of buzzwords, every JavaScript library must support Ajax in its own special way. As a result, almost every library out there has its own abstraction of the XMLHttpRequest object, which saves you from writing your own try-catch statements, supplying request variables in the right way depending on the type of request, and wiring up functions to handle different success and error conditions. Some of them even have handy shortcuts for common Ajax interactions, which can save you from writing code.

For each of the Prototype, Dojo, jQuery, YUI, and MooTools libraries, we’ll translate this low-level Ajax code into the equivalent library syntax:

try
{
  var requester = new XMLHttpRequest();
}
catch (error)
{
  try
  {
    var requester = new ActiveXObject("Microsoft.XMLHTTP");
  }
  catch (error)
  {
    var requester = null;
  }
}

requester.open("GET", "library.php?dewey=005", true);
requester.onreadystatechange = readystatchangeHandler;
requester.send(null);

function readystatchangeHandler()
{
  if (requester.readyState == 4)
  {
    if (requester.status == 200 || requester.status == 304)
    {
      writeUpdate(requester);
    }
  }
}

function writeUpdate(requestObject)
{
  document.getElementById("container").childNodes[0].nodeValue =
      requestObject.reponseText;
}

That code steps through a fairly standard Ajax program:

  1. Create a new XMLHttpRequest object.

  2. Set up a GET connection to a server-side script.

  3. Attach a request variable to the server call.

  4. Send the data to the server.

  5. Monitor the request for completion.

  6. Insert the returned data into an HTML element (by setting the nodeValue of the text node it contains).

This program’s standard functionality should give you a fair indication of the way that each library handles Ajax connections and interactions.

Prototype

Prototype’s approach to handling Ajax calls basically represents the archetype for other libraries. It gives you access to an Ajax object, which offers a couple of methods with which to make requests.

We start a basic Ajax request by calling new Ajax.Request and passing it a number of parameters in the form of an object literal. When you specify an onComplete function, it will automatically be called once the server call has successfully completed:

var requester = new Ajax.Request("library.php",
    {
      method: "get",
      parameters: "dewey=005",
      onComplete: writeUpdate
    });

function writeUpdate(requestObject)
{
  document.getElementById("container").childNodes[0].nodeValue =
      requestObject.reponseText;
}

That code can be further shortened by replacing Ajax.Request with Ajax.Updater. The second method assumes that you will place the contents of responseText directly inside an HTML element (the most common Ajax operation), and allows you to specify the ID of that element. Thus, it circumvents the need for you to create your own callback function:

var requester = new Ajax.Updater("container", "library.php",
    {
      method: "get",
      parameters: "dewey=005",
    });

That’s very succinct!

Dojo

To use Dojo’s Ajax handler, we just call dojo.io.bind with an object literal that contains the appropriate parameters:

dojo.io.bind(
    {
      url: "library.php?dewey=005",
      load: writeUpdate,
      mimetype: "text/plain"
    });

function writeUpdate(type, data, event)
{
  document.getElementById("container").childNodes[0].nodeValue =
      data;
}

There are a couple of tricks with the Dojo Ajax implementation. Specifying mimetype inside the object literal determines what type of data Dojo will pass to your load function when the request is completed (text or XML). The load function receives three arguments:

type

a superfluous variable that always has a value of "load"

data

the only variable you’ll actually use, as it contains the data from the server’s response

event

contains a reference to the low-level transport object that was used to perform the server communication (For the moment, it will inevitably be the XMLHttpRequest object.)

jQuery

As with everything in jQuery, Ajax functionality is available as part of the $ object. $.ajax lets you specify an all-too-familiar object literal with the particular configuration you require for the call:

$.ajax(
    {
      type: "GET",
      url: "library.php",
      data: "dewey=005",
      success: writeUpdate
    });

function writeUpdate(data)
{
  document.getElementById("container").childNodes[0].nodeValue =
      data;
}

Again, with jQuery, as with Dojo, either responseXML or responseText will be passed directly to the success function—the property that’s passed will depend upon the MIME type of the data returned from the server.

YUI

In true Yahoo! UI style, the name that Yahoo! has given to its Ajax object (which it calls a Connection Manager) is rather verbose, but in most other respects it’s similar to what we’ve seen so far:

var handlers = {
  success: writeUpdate
}

YAHOO.util.Connect.asyncRequest(
    "GET",
    "library.php",
    handlers,
    "dewey=005"
);

function writeUpdate(requestObject)
{
  document.getElementById("container").childNodes[0].nodeValue =
      requestObject.reponseText;
}

The handlers variable allows you to specify both the handler function for a successful Ajax request, and the function to be called when an error occurs. These functions are passed a full XMLHttpRequest object, rather than just the data.

MooTools

MooTools based its Ajax handler on the one that comes with Prototype, so both handlers have similar syntax. MooTools’ handler even has the shortcut for placing the returned data directly into an element without specifying a callback function:

var requester = new Ajax("library.php?dewey=005",
    {
      method: "get",
      onComplete: writeUpdate;
    });

requester.request();

function writeUpdate(requestObject)
{
  document.getElementById("container").childNodes[0].nodeValue =
      requestObject.reponseText;
}

The one difference between these two libraries is that the MooTools object doesn’t automatically send the request once it has been initialized; you have to call request when you want to send it.

If you wish to use the element insertion shortcut, the code looks like this:

var requester = new Ajax("library.php?dewey=005",
    {
      method: "get",
      update: "container";
    });

requester.request();

The update property takes the ID of the element whose contents you wish to update.

Summary

Ajax is definitely here to stay, and as users and developers become accustomed to its behavior, the number of ways in which it will be used to enhance web interfaces will only increase.

As you’ve seen in this chapter, the actual communication mechanism of Ajax is relatively straightforward. The pattern of initialize-retrieve-modify draws heavily on all the techniques that you’ve picked up as you’ve worked your way through the preceding seven chapters, so Ajax is the perfect finishing point for all the practical work in this book. But read on to find out where the future of JavaScript might lead you…



[29] An ActiveX object is Microsoft’s term for a reusable software component that provides encapsulated, reusable functionality. In Internet Explorer, such objects normally give client-side scripting access to operating system facilities like the file system, or in the case of XMLHttpRequest, the network layer.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset