Remote Scripting with AJAX, Part 1
Editor's note: The content for this article was excerpted from Cameron's originally published article, which was posted on SitePoint's website last month. SitePoint specializes in publishing fun, practical, and easy-to-understand content for web professionals, and is a distribution partner with O'Reilly Media.
This two-part series of articles covers remote scripting using the AJAX XMLHttpRequest protocol. Part one walks through an example application that demonstrates how to implement the protocol, while part two will show how to create a usable interface.
To begin, download the code archive, which contains all of the files you'll need to create the working examples presented here and for the upcoming second part of this series.
Essentially, remote scripting allows client-side JavaScript to request data from a server without having to refresh the web page. That's it. Everything else that goes into making a seamless web application draws upon the established methods of manipulating the Document Object Model.
However, remote scripting and seamless applications bring with them a host of problems from the desktop application design realm, making those same issues possible on the Web. It's your duty to ensure that your remote scripting interfaces address those issues, and give your users the best possible experience they can get.
Related Reading DHTML
Utopia |
Although XMLHttpRequest is not a public standard, most modern browsers implement it consistently, and it's well on its way to becoming a de facto standard for JavaScript data retrieval. Internet Explorer 5 for Windows, Mozilla 1.0, Safari 1.2 and the upcoming version 8.0 of Opera all introduce XMLHttpRequest as an available object.
The Internet Explorer XMLHttpRequest API is available for download.
You can also download the Mozilla documentation.
If you require support for browsers that are older than these, methods using iframes provide a viable solution; however, coding for these browsers will also limit your ability to utilize standard JavaScript DOM methods. This article will focus on the more contemporary XMLHttpRequest method.
In order to demonstrate how to use the XMLHttpRequest protocol inside of a remote scripting application, I've created a simple, one-page example. It assumes that JavaScript and XMLHttpRequest are available in order to make the code more readable, but in any real-world application, you should always check that XMLHttpRequest is available and have a fallback (i.e., normal form submission) where it is not.
The example application will allow the user to send a free ecard to a friend's email address. To do this, the user has first to enter a receipt number, which they received when they purchased goods previously, and which has since been stored in the database of ExampleCo. Then, the user must complete the remaining fields before the ecard is sent, entering the recipient's email address, the message, and the graphic image that will be used for the card:
Remote scripting is used for three actions in this example, to:
Along with these actions, the example contains JavaScript, which validates the other form fields before submission, and allows the user to select an ecard graphic.
The example has been created in two separate versions. In this article, the first of these versions will demonstrate the implementation of the XMLHttpRequest protocol inside of an application, but it contains several less-than-desirable usability problems. These problems are tackled in the second example coming up in part two, which aims to highlight some of the issues that can be encountered as you move from a page-based application model towards a more dynamic and interactive environment.
by Cameron Adams
In a traditional server/client application, the entire ecard form would have to be submitted to the server, checked, and returned to the browser before the client could be made aware of whether their receipt number was valid or not. With the remote scripting model, we're able to check the receipt number as soon as the user has finished dealing with that field. When a user submits the form, the browser has already identified whether or not the data is valid.
The first step in checking the data remotely is to know when the user has
entered a value into the receipt number field. This can be detected using an
onchange
event handler for the field. A "change" on a text field is
registered whenever the user modifies the value of the text field and then
"blurs" away from that field (i.e., they tab or click away from it). This is
normally a good indication that a user has finished filling out the field, and
that the data it contains can be processed. By capturing this
onchange
event, we can tell our script to begin validating the
field's content:
receipt.onchange = onchangeReceipt;
onchangeReceipt
is a function that is called when the
onchange
event is triggered. It's inside of this function that we
initialize our XMLHttpRequest object and send off the relevant data to be
checked:
var requester = null;
function onchangeReceipt()
{
/* Check for running connections */
if (requester != null && requester.readyState != 0 && requester.readyState
!= 4)
{
requester.abort();
}
try
{
requester = new XMLHttpRequest();
}
catch (error)
{
try
{
requester = new ActiveXObject("Microsoft.XMLHTTP");
}
catch (error)
{
requester = null;
return false;
}
}
requester.onreadystatechange = requesterExecuteAction;
requester.open("GET", "receipt.php?receipt=" +
this.value);
requester.send(null);
return true;
}
You might recognize some of that syntax from the first part of this article;
namely, the forked try
/catch
structure, and the
open()
and send()
methods that control the
XMLHttpRequest object.
The first if
statement checks to see whether or not an
XMLHttpRequest object already exists and is currently running; if so, it aborts
that connection. This ensures that a number of conflicting XMLHttpRequest calls
aren't run simultaneously, which would clog up the network. The function then
continues on, to create a new XMLHttpRequest object and open a connection to the
server-side validation script, receipt.php.
In receipt.php, the CGI variable receipt is checked and, if its
value is "1234567"
, some XML data is
returned; otherwise, a plain text string of "empty"
is returned,
indicating that the receipt number is invalid:
if ($receipt == "1234567")
{
header("Content-type: text/xml");
$filePointer = fopen("example.xml", "r");
$exampleXML = fread($filePointer, filesize("example.xml"));
fclose($filePointer);
print($exampleXML);
}
else
{
header("Content-type: text/plain");
print("empty");
}
Hard-coded values and data have been used in this example to simplify the code, but in the real world, this PHP script would check the receipt number against a database, and return the appropriate data for that number.
Note that if receipt number is invalid, the content-type header sent is
"text/plain"
. This simplifies the message-printing process
somewhat, but it also means that on the client side, the
responseXML
property of the XMLHttpRequest object will not contain
anything. As such, you should always be aware of what your server-side scripts
return, and keep an eye on responseXML
or responseText
appropriately.
by Cameron Adams
As well as calling the server-side script, onchangeReceipt()
also assigns onreadystatechangeReceipt()
to monitor the status of
the connection via the onreadystatechange
event, and it is this
function that determines when the connection is finished and further action
should be taken. To do this, we use the previously discussed
readyState
/status
condition nesting:
function onreadystatechangeReceipt()
{
/* If XMLHR object has finished retrieving the data */
if (requester.readyState == 4)
{
/* If the data was retrieved successfully */
if (requester.status == 200)
{
writeDetails();
}
/* IE returns a status code of 0 on some occasions, so ignore this
case */
else if (requester.status != 0)
{
alert("There was an error while retrieving the URL: " + requester.statusText);
}
}
return true;
}
When a successful status code is returned, writeDetails()
is
invoked. It is this function that parses the returned data and determines what
to do to the web page:
function writeDetails()
{
var receipt = document.getElementById("receipt");
if (requester.responseText.charAt(0) == "<")
{
var email = document.getElementById("email");
var name = document.getElementById("name");
receipt.valid = true;
email.value = requester.responseXML.getElementsByTagName("email")[0].
childNodes[0].nodeValue;
}
else
{
receipt.valid = false;
}
return true;
}
This function firstly checks the responseText
property of the
XMLHttpRequest object, to see whether the receipt number was valid or not. If it
is valid, the data will be in XML format and its first character will be an
opening angled bracket (<
); otherwise, it will be a plain
string. In each case, the extended property valid
is set
appropriately on the receipt number field. Additionally, if the receipt number
is valid, extra data is added to the email field, having been parsed from the
responseXML
property of the XMLHttpRequest object.
The execution of writeDetails()
marks the end of the remote
scripting process for receipt number validation. With the extended
valid
property set on the field, the browser knows whether or not
the data is OK, and can alert users of any errors when they try to submit the
form:
orderForm.onsubmit = checkForm;
function checkForm()
{
if (!receipt.valid)
{
receipt.focus();
alert("Please enter a valid receipt number.");
return false;
}
...
If there is an error with the form, an alert()
dialog appears
when the submit button is clicked, asking the user to correct the error before
the form is submitted:
checkForm()
also handles the submission of the form data via
remote scripting (though, in reality, normal form submission would probably
suffice for an application like this). The remote scripting for the data
submission uses the same code we used for validation, but a different
server-side script is supplied to process the data, and instead of
onreadystatechangeReceipt()
being called once the connection has
finished, onreadystatechangeForm()
is called.
onreadystatechangeForm()
triggers sentForm()
to
re-write the web page and inform the user that the ecard was either successfully
or unsuccessfully sent, depending upon the data returned from the server:
function sentForm()
{
var body = document.getElementsByTagName("body")[0];
body.innerHTML = "<h1>Send someone an e-card from ExampleCo!</h1>";
if (formRequester.responseText == "success")
{
body.innerHTML += "<h1>Send someone an e-card from ExampleCo!</h1><p>Your
ExampleCo e-card has been sent!</p>";
}
else
{
body.innerHTML += "<p>There was an error while sending your
ExampleCo e-card.</p>";
}
return true;
}
This removes the initial form presented to the user, and inserts a final status message:
Figure 3.
While this application rewrites almost the whole page, it's easy to see how specific parts of the DOM could be changed using remote scripting, which would enable separate parts of an application interface to update independently of the web page itself.
Stay tuned for part two next week, where Cameron will cover how to create a usable scripting interface for the example application.
XML.com Copyright © 1998-2005 O'Reilly Media, Inc.