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!
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 iframe
s, 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.
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.
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();
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:
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.
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).
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:
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.
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.
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]");
Content-Type
RequiredOpera 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.
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.
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"
.
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.
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.
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.
The HTML for the widget looks like this:
<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:
Initialize event listeners.
Handle event triggers.
Create an XMLHttpRequest
connection.
Parse data.
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:
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) } }
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:
<?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:
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); }
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.
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:
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
:
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.
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:
<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:
Override the default form submission behavior.
Get the form data.
Submit the form data to the server.
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:
var ContactForm = { init: function() { var contactForm = document.getElementById("contactForm"); Core.addEventListener(contactForm, "submit", ContactForm.submitListener); },
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:
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) } },
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:
This time, when we | |
Remember that we have to set the | |
In the
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. | |
Similarly, if the server does not successfully receive the
contact information, | |
With that last method in place, the
| |
The very last command in |
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!
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:
Create a new XMLHttpRequest
object.
Set up a GET connection to a server-side script.
Attach a request variable to the server call.
Send the data to the server.
Monitor the request for completion.
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’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!
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.)
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.
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 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.
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.