Ajax (Asynchronous JavaScript and XML) is not so much a new technology
as a set of existing technologies used together in a new way, bound together
by a mechanism that lets you communicate between the browser and server
without reloading the entire page. In its most fundamental form, Ajax
requires that you understand only one new piece: the XMLHttpRequest
object in JavaScript (or its
equivalent depending on the browser). This is the object that allows you to
make a connection back to the originating server to request additional data.
Once you receive the data, you can use it to adjust only the portion of the
page that you need to update.
Although the object’s name and the term “Ajax” itself imply
that XML is the only format for exchanging data, there are others.
JSON is especially good because it lets you pass a string of
JavaScript on which you call json_parse
(which you can download from http://json.org/json_parse.js), to yield a JavaScript
object.
Certain practices simplify working with Ajax. Usually, it’s helpful to
load a library that abstracts the XMLHttpRequest
object. Fortunately, there are
several libraries that help with this. In addition, within the browser, the
MVC design pattern is a good model for maintaining a clear separation
between data changes and updates to a presentation. On the server, the same
principles discussed in Chapter 6 for managing data
for complete pages can also provide a good structure for data in Ajax
requests. These ideas are captured in the following tenet from Chapter 1:
Tenet 8: Large-scale Ajax is portable and modular, and it maintains a clear separation between data changes and updates to a presentation. Data exchange between the browser and server is managed through a clearly defined interface.
This chapter is divided broadly into three sections: the first explores Ajax within the browser, the second explores Ajax on the server, and the third illustrates Ajax with MVC. We’ll begin by discussing the fundamentals of a simple Ajax transaction and look at comparisons between basic Ajax requests in some popular libraries. The libraries we’ll examine are Dojo, jQuery, Prototype, and YUI. On the server, we’ll explore common formats for data exchange, server proxies, and techniques for handling Ajax requests in a modular fashion. Finally, we’ll look at a set of prototype objects in JavaScript to support MVC and explore two examples of Ajax with MVC. One is a simple control panel that has multiple views; the other is an illustration of accordion lists.
Like most transactions that take place on the Web, Ajax transactions consist of two coordinated sets of operations: one in the browser and the other on the server. In this section, we look at some of the fundamentals for working with Ajax in the browser.
Ajax employs JavaScript to establish a connection to the server from a web page and load additional data into the page. Example 8-1 demonstrates the JavaScript to establish the simplest of Ajax requests. The libraries that virtually all developers use hide most of these logistics, but you should understand them in order to use Ajax effectively.
In the example, handleConnect
is a
method that you can call, perhaps triggered by an event handler, to
initiate an Ajax request. As the “A” in Ajax signifies, requests are
normally asynchronous (you do have the option to make them synchronous,
but this is ill-advised), leaving your code with the job of determining
when the response has arrived from the server. JavaScript offers this
information as a state change in the request. The handleConnect
method creates an instance of
the XMLHttpRequest
object, specifies
a method that JavaScript will call as the request’s state changes,
configures the request, and, finally, sends the request.
The handleRequest
method in
Example 8-1 is the method called whenever the
state of the request changes. To make sure its operations take place
when the request is in the right state—when data has arrived from the
server—the method checks whether the readyState
member is set to 4 and the status
member is set to 200 (there are other
states with other meanings, but we won’t explore those here). When the
state is 4, the status is 200, and there is data from the server in XML
format, the responseXML
member will
have been populated with a complete DOM constructed from the XML. In
this case, you can use JavaScript DOM methods to access the data you
need. For example, to get all
elements that have a specific tag, you can invoke responseXML.getElementsByTagName
. If the
server sends plain text or text to be interpreted as JSON, the responseText
member is populated with a
string. If you expect a response in JSON format, pass the text to
eval
, or more safely,
json_parse
to get a valid JavaScript
object. Example 8-1 illustrates working
with JSON data in responseText
.
Although eval
is
fast, it’s important to recognize that it will execute any piece of
JavaScript, even those that could contain malicious code. If you have
complete trust and control of the JSON data you’re evaluating,
eval
may be acceptable; however,
json_parse
is more secure because
it recognizes only JSON text.
function handleRequest() { // The request is in the proper state for us to handle it only when // is readyState member has been set to 4 and its status shows 200. if (this.readyState == 4 && this.status == 200) { if (this.responseXML != null) { // This is the response member to read for XML; it holds a DOM. ... } else if (this.responseText != null) { var data; // This is the response member to read for JSON data (or text). data = json_parse(this.responseText); // For illustration, just show the message in an alert dialog. // The response is an object containing one member: a message. alert(data.message); } } } function handleConnect() { var req; // Create the request object and set up the handler for state changes. req = new XMLHttpRequest(); req.onreadystatechange = handleRequest; // Set up the type of request, where it should go, and do the request. req.open("GET", "service.php..."); req.send(); }
For the sake of viewing what this example looks like end to end,
Example 8-2 shows the PHP server code for
service.php, the script used to handle the Ajax
request from Example 8-1. It returns a JSON
object with one member called message
.
<?php // Create a PHP hash containing the string to return as the response. $data = array ( "message" => "Hello" ); // Encode the PHP data structure so that it becomes a JSON structure. $json = json_encode($data); // Set the content type to inform that we're sending a JSON response. header("Content-Type: application/json"); // Send the JSON response. print($json); ?>
As you can see, carrying out a simple Ajax transaction is
not very difficult. That said, Ajax becomes more complicated when you
consider that prior to Internet Explorer 7.0, there were serious
interoperability issues among the major browsers. These
included inconsistent or missing XMLHttpRequest
objects, memory leaks, and
other implementation details. In addition, a real Ajax application
typically requires a lot more management than the simple steps
illustrated in Examples 8-1 and 8-2.
Fortunately, there are a number of libraries today that help with this
and that standardize support for Ajax across the major browsers. Other
techniques for fetching data asynchronously besides using the XmlHttpRequest
object include using iframe
elements and
script
nodes as the transport mechanism.
In the approach using iframe
elements, you hide an iframe
on your
original page. Then, whenever you need additional data, you use
JavaScript to alter the location to which the iframe
element points. The request returns
whatever data you need in its own DOM, which your original page can
access. The use of iframe
elements is
one of the original ways in which web developers implemented Ajax, so
you may see it when working with Ajax applications that have been around
for a while.
In the script
node approach,
whenever you need additional data, you use JavaScript to add a script
node with a src
attribute that points to a page that
fetches whatever data you need as a set of JavaScript objects. The
objects returned in that request are accessible by the original
page.
Although clever, iframe
and
script
approaches can be difficult to
manage in large web applications because both approaches require that
you write some custom code to abstract and coordinate the transport
layer itself between the HTML and JavaScript. Now that other support for
Ajax is so widely available, there is little need for these approaches,
except in some very specific applications.
In this section, we explore several Ajax libraries that can help manage the complexities of large Ajax applications while standardizing how Ajax works across the major browsers. These include Dojo, jQuery, Prototype, and the YUI library. Specifically, we’ll look at how each library supports fundamental GET and POST requests for comparison purposes. Of course, this is far from a complete depiction of what the libraries can do. For example, they all offer various options for carrying out requests, support flexible data formats, and define numerous events for which you can provide handlers, which we only touch on here.
Dojo is a JavaScript library built on several contributed code bases. You can download the library and read its complete documentation at http://www.dojotoolkit.org:
The following method executes an Ajax GET request
with Dojo. The method accepts one object as a parameter; the
most commonly used members of the object are shown below. The
handleAs
member can be
text
, xml
, or json
, indicating that the data
argument passed to the function
specified for load
is a
string, DOM, or JSON object, respectively. The url
member is the destination for the
request. The timeout
member
is measured in milliseconds:
dojo.xhrGet ( { url: "service.php?key1=val1&key2=val2&...", timeout: 5000, handleAs: "json", load: function(data, args) { // Do what is needed when the Ajax call returns successfully. }, error: function(error, args) { // Do what is needed when the Ajax call returns on a failure. } } );
The following method executes an Ajax POST request
with Dojo. The parameters for the method are the same as
described for GET except that you set the data to post as an
object in the content
member:
dojo.xhrPost ( { url: "service.php", timeout: 5000, handleAs: "json", content: { "key1": "val1", "key2": "val2", ... }, load: function(data, args) { // Do what is needed when the Ajax call returns successfully. }, error: function(error, args) { // Do what is needed when the Ajax call returns on a failure. } } );
The jQuery library is another JavaScript library with especially good documentation for its Ajax support. You can download the library and read its complete documentation at http://www.jquery.com:
The following method executes an Ajax GET request with
jQuery. The method accepts one object as a parameter whose most
common members are shown below. The dataType
member can take a number of
values, of which the most common are text
, xml
, or json
, indicating that the data
argument passed to the function
specified for success
is a
string, DOM, or JSON object, respectively. The url
member is the destination for the
request. You can specify the query parameters for the GET as an
object in the data
member.
The timeout
member is
measured in milliseconds:
jQuery.ajax ( { url: "service.php", type: "GET", timeout: 5000, data: { "key1": "val1", "key2": "val2", ... }, dataType: "json", success: function(data) { // Do what is needed when the Ajax call returns successfully. }, error: function(xhr, text, error) { // Do what is needed when the Ajax call returns on a failure. } } );
The following method executes an Ajax POST request with
jQuery. The parameters for the method are the same as described
for GET except you set the type
member to POST
:
jQuery.ajax ( { url: "service.php", type: "POST", timeout: 5000, data: { "key1": "val1", "key2": "val2", ... }, dataType: "json", success: function(data) { // Do what is needed when the Ajax call returns successfully. }, error: function(xhr, text, error) { // Do what is needed when the Ajax call returns on a failure. } } );
Prototype is one of the earliest of the JavaScript libraries to support Ajax. You can download the library and read its complete documentation at http://www.prototypejs.org:
The following method executes an Ajax GET request with
Prototype. The method accepts two parameters: the destination
for the request and an object whose most common members are
shown below. In the handler specified by onSuccess
, you access transport.responseText
for responses
using plain text. For XML responses, transport.responseXML
will have been
populated with a DOM that you can access with JavaScript DOM
methods. For JSON responses, the transport.responseJSON
member
will have been populated with the JavaScript object that is the
result of the evaluated response text. You specify the query
parameters for the GET as an object in the parameters
member:
Ajax.Request ( "service.php", { method: "get", parameters: { "key1": "val1", "key2": "val2", ... }, onSuccess: function(transport) { // Do what is needed when the Ajax call returns successfully. }, onFailure: function(transport) { // Do what is needed when the Ajax call returns on a failure. } } );
The following method executes an Ajax POST request with
Prototype. The parameters
for the method are the same as described for GET except you set
the method
member to post
:
Ajax.Request ( "service.php", { method: "post", parameters: { "key1": "val1", "key2": "val2", ... }, onSuccess: function(transport) { // Do what is needed when the Ajax call returns successfully. }, onFailure: function(transport) { // Do what is needed when the Ajax call returns on a failure. } } );
The YUI library was developed at Yahoo! for use both within Yahoo! and by the world’s web development community. You can download the library and read its complete documentation at http://developer.yahoo.com/yui.
As this book was being completed, YUI 3 was in beta development. The information below
pertains to versions prior to this. One of the big differences
between YUI 2 and YUI 3 is the YUI
object, which places YUI 3 features in
their own namespace. This lets you transition from YUI 2 to YUI 3
without having to change all your code at once.
The following method executes an Ajax GET request using
the YUI Connection Manager. The method accepts three parameters:
the request type, the destination for the request, and an object
whose most common members are shown below. In the handler
specified by success
, you
access o.responseText
for
responses in plain text. For XML responses, o.responseXML
will have been populated
with a DOM that you can access
with JavaScript DOM methods. For JSON, you need to
evaluate the result in o.responseText
yourself. The argument
member is used to pass
whatever arguments you’d like to the handler methods. The
timeout
member is measured in
milliseconds:
YAHOO.util.Connect.asyncRequest ( "GET", "service.php?key1=val1&key2=val2...", { success: function(o) { // Do what is needed when the Ajax call returns successfully. }, failure: function(o) { // Do what is needed when the Ajax call returns on a failure. }, timeout: 5000, argument: { key1: val1, key2: val2, ... } } );
The following method executes an Ajax POST request using
the YUI Connection Manager. The parameters for the method are
the same as described for GET except you set the first parameter
to POST
and add a fourth
parameter containing a string of key-value pairs for the POST
data:
YAHOO.util.Connect.asyncRequest ( "POST", "service.php", { success: function(o) { // Do what is needed when the Ajax call returns successfully. }, failure: function(o) { // Do what is needed when the Ajax call returns on a failure. }, timeout: 5000, argument: { key1: val1, key2: val2, ... } }, "key1=val1&key2=val2..." );
Once an Ajax request is executed in the browser, it’s up to the server at the other end of the connection to handle the request. This section covers three important issues in writing the server’s side of the transaction: choosing a data format, using a server proxy, and applying techniques that promote modularity.
The primary formats used to send data in Ajax responses are plain text, XML, and JSON. Of course, whichever format you choose, the JavaScript that you provide to handle responses must be prepared to work with that format, regardless of whether the library detects the format automatically or requires you to specify the format explicitly. In this section, we explore the various formats for data returned by the server and look at how to work with each of them in PHP.
Ajax responses in plain text are simply strings returned by the server. Generally, a response from the server using plain text is not very useful, because it’s largely unstructured. For anything but the simplest requests, this becomes unnecessarily difficult to deal with in the browser. Example 8-3 illustrates generating a plain-text response to an Ajax request in PHP.
<?php // Handle the inputs via $_GET or $_POST based on the request method. ... // Assemble whatever data is needed to form the appropriate response. ... $text = <<<EOD ... EOD; // Set the content type to specify that we're giving a text response. header("Content-Type: application/text"); // For Ajax data that is very dynamic (which is often the case), you // can set Expires: 0 to invalidate cached copies in future requests. header("Expires: 0"); // Send the text response. print($text); ?>
Ajax responses in XML are highly structured; however,
they can be verbose for the amount of real data that they actually
contain. When you receive an XML response in the browser, the response
is presented in the form of a DOM with which you can use all the
normal DOM methods provided by JavaScript. For example, to get all the
elements in the document with a specific tag, you can use document.getElementsByTagName
. Because DOM
methods can have an impact on performance, it’s important to structure
your XML data with performance in mind. For example, keep the
important data near the surface of the XML hierarchy or near a node
that is accessible by ID (if you can get close to an element using
document.getElementById
, you only need to
search that node’s descendants). Example 8-4 illustrates
generating an XML response to an Ajax request in PHP. You can find
more about working with XML data in Chapter 6.
<?php // Handle the inputs via $_GET or $_POST based on the request method. ... // Assemble whatever data is needed to form the appropriate response. ... $xml = <<<EOD <?xml version="1.0"?> ... EOD; // Set the content type to inform that we're sending an XML response. header("Content-Type: application/xml"); // For Ajax data that is very dynamic (which is often the case), you // can set Expires: 0 to invalidate cached copies in future requests. header("Expires: 0"); // Send the XML response. print($xml); ?>
Ajax responses in JSON are also highly structured;
however, since JSON is actually nothing more than just the normal
JavaScript syntax for object literals, and a JavaScript object is
exactly what we need in the browser, JSON is a great fit for Ajax.
When you receive a JSON response in the browser, you evaluate it using
json_parse
(if the library hasn’t
done so already for you). After this, you access the data just like
any other JavaScript object. Example 8-5 illustrates
generating a JSON response to an Ajax request in PHP. You can find
more about transforming a PHP data structure to JSON in Chapter 6.
<?php // Handle the inputs via $_GET or $_POST based on the request method. ... // Assemble whatever data is needed to form the appropriate response. ... $data = array ( ... ); // Encode the PHP data structure so that it becomes a JSON structure. $json = json_encode($data); // Set the content type to inform that we're sending a JSON response. header("Content-Type: application/json"); // For Ajax data that is very dynamic (which is often the case), you // can set Expires: 0 to invalidate cached copies in future requests. header("Expires: 0"); // Send the JSON response. print($json); ?>
When you specify the destination for an Ajax request, it’s critical to remember that the destination must be on the same server that served the original page. In fact, some browsers will not allow you to specify a hostname within the destination at all, even if it’s the same as the originating server. As a result, avoid the usual http://hostname prefix with Ajax URLs.
This same-origin policy prevents a type of security issue known as cross-site scripting (XSS), wherein code is injected by a malicious visitor into a page viewed by another visitor, unbeknownst to the initial visitor. Without such a policy for Ajax requests, malicious code could cause visitors of one page to interact unknowingly with an entirely different server. The issue is especially insidious for Ajax requests because they usually take place quietly behind the scenes.
Fortunately for benevolent developers, there is a safe and easy way to have Ajax requests handled by a different server from the one that sent the page: place a server proxy on the originating server through which all Ajax requests pass (i.e., you use the path of the proxy in the requests) before being routed to their true destination. This is permitted because requests first must pass through a server presumably under your control (the original server). If visitors trust your site, the assumption is that they are willing to trust other servers with whom you are communicating. Of course, others may contact your proxy server directly to use it as a pass-through. As a result, in many situations you’ll want to examine who is making each request to determine whether or not it should be allowed to utilize the proxy. Example 8-6 is a server proxy for Ajax requests written using cURL in PHP.
<?php // Retrieve parameters passed to the proxy using GET. For this example, // we're assuming that Ajax requests are being made using the GET method. $query = ""; for ($_GET as $key => $val) { if (!empty($query)) $query .= "&"; $query .= $key."=".$val; } if (!empty($query)) $query = "?".$query; // Set up the host and script that you really want for the Ajax request. $host = "..."; $proc = "..."; $url = $host.$proc.$query; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, false); // Set the last value to true to return the output rather than echo it. curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); header("Content-Type: application/json"); curl_exec($ch); curl_close($ch); ?>
As the amount of data that you need to manage for an Ajax application increases, so does the need to have good techniques to manage the complexity on the server. Fortunately, the techniques we presented using data managers in Chapter 6 also work well when managing data for Ajax requests.
Recall from Chapter 6 that a data manager
is an object that abstracts and encapsulates access to a specific set of
data. Its purpose is to provide a well-defined, consistent interface by
which you can get and set data in the backend, and to create a clear
structure for the data itself. Chapter 6 also
demonstrates how to combine and extend data managers using inheritance
or aggregation. Recall that because a data manager is an object,
anywhere you need to get the data it manages, you simply instantiate the
data manager and call its get_data
method. You
can do this for Ajax, too.
Since data managers return data in associative arrays in PHP, one additional step that you
need to perform for Ajax requests is to transform the data into a format
suitable for Ajax responses. This is easy if the desired format is JSON,
since all you need to do is pass the data structure returned by the data
manager to json_encode
. For XML, the
process is not so simple. Because the steps required to transform data
to XML are usually specific to the data itself, it often makes sense to
encapsulate the support for this directly within the data manager and
enable it with a parameter as you need it.
At times, you may want data marked up in HTML on the
server. In this case, you can return the HTML within a member of a JSON
object. Then, within the browser, insert it into the DOM using the
innerHTML
member of the node that you
wish to make its parent. This approach may be beneficial when changes to
the DOM resulting from an Ajax request are fairly complicated and you
already have a lot of code on the server to construct the HTML. Rather
than writing the code again in JavaScript, you can use the existing
server code. Also, multiple calls to DOM methods in JavaScript,
depending on the number and what they do, may result in slower
performance than letting the browser rebuild the DOM for you after
setting an element’s innerHTML
member.
Example 8-7 presents an
example of an Ajax service that uses a data manager to return data for
new car listings in the JSON format. The data manager follows the
practices outlined for data managers in Chapter 6. For example, it uses the new_car_listings
member to keep the inputs and
outputs for the data manager uniquely identifiable should there be
multiple data managers in use. The example first sets up the arguments
for NewCarListingsDataManager
, then
calls the data manager’s get_data
method to populate $load_data
. Once
this method returns, it uses json_encode
to convert the data in $load_data
to JSON.
<?php require_once(".../datamgr/nwclistings.inc"); ... $load_args = array(); $load_data = array(); $load_stat = array(); // Handle inputs for the car query, starting point, total count, etc. if (!empty($_GET["nwcqrymake"]) $load_args["new_car_listings"]["make"] = $_GET["nwcqrymake"]; else $load_args["new_car_listings"]["make"] = ""; // There would likely be several other query parameters handled here. ... // The following arguments presumably come from a pagination module. if (!empty($_GET["pgnbeg"]) $load_args["new_car_listings"]["begin"] = $_GET["pgnbeg"]; else $load_args["new_car_listings"]["begin"] = 0; if (!empty($_GET["pgncnt"]) $load_args["new_car_listings"]["count"] = $_GET["pgncnt"]; else $load_args["new_car_listings"]["count"] = 10; // Call upon whatever data managers are needed to create the response. $dm = new NewCarListingsDataManager(); $dm->get_data ( $load_args["new_car_listings"], $load_data["new_car_listings"], $load_stat["new_car_listings"] ); ... // Confirm that no errors occurred; adjust the response accordingly. ... // Encode the PHP data structure so that it becomes a JSON structure. $json = json_encode($load_data); // Set the content type to inform that we're sending a JSON response. header("Content-Type: application/json"); // An Expires header set to 0 eliminates caching for future requests. header("Expires: 0"); // Return the JSON data. print($json); ?>
Even with the help of Ajax libraries in the browser, large Ajax applications often have to manage complicated interactions. For example, changes to a single data source often require coordinated updates to several components in the user interface. In addition, the relationships between components in the user interface and their data sources may change over the lifetime of the application. MVC (Model-View-Controller) is a design pattern that can help address these issues. It does this by defining a system of distinct, loosely-coupled components: models, views, and controllers. Models are responsible for managing data, views are responsible for managing various presentations of the data, and controllers are responsible for controlling how the models and views change. In this section, we explore how MVC can help make the complexities of a large Ajax application more manageable. In the process, we’ll also observe more of the Ajax operations presented earlier in action.
MVC is based on another design pattern, Publisher-Subscriber. The main idea
behind Publisher-Subscriber is that a publisher maintains a list of
subscribers that should be notified whenever something in the publisher
changes. Publishers normally implement at least three methods: subscribe
, unsubscribe
, and notify
. Subscribers call
the subscribe
method to
register for notifications; subscribers call the unsubscribe
method to
tell publishers that they no longer want the notifications; and the
publisher itself calls the notify
method whenever the publisher needs to notify its list of subscribers
about a change. The main method that subscribers implement is update
. The publisher
calls the update
method within notify
to give a subscriber the chance to update
itself about a data change.
In the context of Publisher-Subscriber, models are publishers and views are subscribers. As publishers, models manage data and notify subscribed views whenever changes happen in the model. As subscribers, views subscribe to models and update the presentations they manage whenever they are notified by the models about changes to their data. The remarkable accomplishment of MVC, which it derives from Publisher-Subscriber, is that every time the data for a model changes, the views update themselves automatically. Furthermore, components responsible for the data and the presentations are loosely coupled—that is, one doesn’t need to know about the other, so they are easier to maintain over an application’s lifetime.
In the context of Ajax, models manage Ajax connections and store the resulting data. Views are notified of those changes and update themselves by modifying the parts of the DOM for which they are responsible. A good granularity for views is to make them correspond to modules (see Chapter 7). Interestingly, MVC is also helpful for DHTML applications that don’t use Ajax but have other dynamic aspects to manage. The only difference is that changes to the model happen as a result of local data changes rather than from making an Ajax connection.
To better understand how MVC specifically aids Ajax
applications, let’s look at a basic example for testing simple Ajax requests managed using MVC. In this
example, we’ll use one model, TestModel
, to which two views are subscribed.
Each view is an instance of TestView
.
For simplicity, the model manages a single piece of data: a timestamp
that can be set via the server using an Ajax request or locally within
the browser. Whenever the time is updated (either from the server or
locally), each view updates itself to reflect the new timestamp in the
model.
The implementation consists of three classes, shown in their entirety in this chapter, built on top of some libraries provided by YUI. Naturally, you can create the same basic structure using other JavaScript libraries.
Figure 8-1 shows the user interface for this application. The shaded area at the top of the figure is the first view; the shaded area at the bottom is the second. The lighter area in the middle is a control panel that contains several buttons that let you initiate various actions. In addition to the basic actions for making an Ajax request or local JavaScript call to set the time, the application lets you experiment with several other features, such as handling communication failures, data failures, timeouts, aborted requests, and collisions between concurrent requests. The following provides more detail about each of the actions in the application.
Sets the time in the model by making a local
JavaScript call; no Ajax request is made. When you click this
button, each view updates itself to display the message “Hello
from the browser at time
,” which contains the
current time in the browser. The Local button demonstrates the
usefulness of MVC even for managing changes to a model without
Ajax.
Sets the time in the model to the time on the server
using an Ajax request to retrieve the time. When you click this
button, each view updates itself to display the message “Hello
from the server at time
,” which contains the
current time on the server.
Simulates a communications failure (to show how the application would handle a bad URL, for example). When you click this button, the application responds with an alert.
Simulates a failed attempt to get data (to show how the application would handle, for example, unexpected data from the server). When you click this button, the application responds with an alert.
Causes the server to take too long to respond, which causes a timeout to occur. When you click this button, the application responds with an alert.
Aborts the current request. To test this, first click the button to test timeouts, then without giving it enough time to time out, click the button to abort the request; you won’t get the alert for the timeout because the request is aborted before the timeout occurs.
Changes how collisions between concurrent requests are handled. The default state (Ignore) is to ignore subsequent Ajax requests within a model until the current one completes. Each time you click this button, you toggle between this policy and one that allows subsequent requests to cancel current ones (Change). A good example of the Change policy is an autocomplete application wherein a server is contacted for entries that match strings you type as you type them (e.g., type an entry into the Google or Yahoo! search box). If you type the next character of the entry before the server has a chance to respond with matches, only the latest request is needed (when typing pauses long enough). To test either policy, first click the button to test timeouts, then without giving it enough time to time out, make a remote request. If the Ignore policy is in place, you will see the alert for the first request that timed out. On the other hand, if the Change policy is active, you will get a response from the second request that canceled the first.
Example 8-8 presents the
HTML for the Ajax application shown in Figure 8-1. Each button
passes the appropriate action as a string argument (loc
for Local, etc.) to the model using the
handleAction
method. You call
model.init
to initialize the model
and notify the views that there is something to display. The model
object is defined in Example 8-10.
<body> <div id="testview1"> </div> <div id="actions"> <input id="loc" type="button" value="Local" onclick="handleAction ('loc')," /> <input id="rmt" type="button" value="Remote" onclick="handleAction ('rmt')," /> <input id="bad" type="button" value="Fail" onclick="handleAction ('bad')," /> <input id="dat" type="button" value="Data" onclick="handleAction ('dat')," /> <input id="tmo" type="button" value="Timeout" onclick="handleAction ('tmo')," /> <input id="abt" type="button" value="Abort" onclick="handleAction ('abt')," /> <input id="pol" type="button" value="Policy" onclick="handleAction ('pol')," /> </div> <div id="testview2"> </div> <script type="text/javascript"> // Initialize the model to a start value, which notifies the views too. model.init(); </script> </body>
Example 8-9 shows how
to define the model and view objects for the example. The TestModel
and TestView
objects are derived from the
prototype objects Model
and View
, respectively. You’ll see more about
these in a moment. For now, notice that the model implements three
methods: init
to set the initial
state of the model and notify the views, abandon
to handle timeouts, and recover
to handle other failures. These
methods are part of Model
’s abstract
interface. The TestView
object
implements one method, update
, to
define what to update when the state of the model changes. This method
is part of View
’s abstract
interface.
TestModel = function() { MVC.Model.call(this); }; // TestModel objects are derived from the Model object. TestModel.prototype = new MVC.Model(); TestModel.prototype.init = function() { // The state member of a model stores the current data for the model. this.state = { "message": "Initial message" }; // Only the setState method does notifications automatically for you. this.notify(); }; TestModel.prototype.abandon = function() { // Implement this method to do whatever is needed to handle timeouts. alert("Called abandon to handle communications timeout."); }; TestModel.prototype.recover = function() { // Implement this method to do whatever is needed to handle timeouts. alert("Called recover to handle communications failure."); }; TestView = function() { MVC.View.call(this); }; // TestView objects are derived from the View object. TestView.prototype = new MVC.View(); TestView.prototype.update = function() { // The id member is the containing element for the view in the DOM. var element = document.getElementById(this.id); // Whenever a view updates itself, it needs the state of its model. msg = this.model.getState().message; // Do the actual update for keeping this view current with the model. element.innerHTML = msg; };
Example 8-10 shows how to
use the model and view objects that we just defined. First, instantiate
the model, then attach it to the views for which you would like to be
notified of state changes. Any changes that take place in the model will
cause a corresponding call to TestView.update
. Example 8-10 also implements handleAction
, which sets the state of the
model based on buttons you click in the control panel. The setState
method of Model
lets you set the state of the
model.
// This is the model that will keep track of the time last retrieved. var model = new TestModel(); // Set a short connection timeout just to speed up the testing case. model.setTimeout(2000); // Create each view and attach the model. Attaching subscribes the view. var view1 = new TestView(); view1.attach(model, "testview1"); var view2 = new TestView(); view2.attach(model, "testview2"); // This method handles the various actions by which you change the model. function handleAction(mode) { switch (mode) { case "loc": // Create a local timestamp without performing an Ajax request. var d = new Date(); var h = ((h = d.getHours()) < 10) ? "0" + h : h; var m = ((m = d.getMinutes()) < 10) ? "0" + m : m; var s = ((s = d.getSeconds()) < 10) ? "0" + s : s; var t = h + ":" + m + ":" + s; // Update the model locally with the timestamp from the browser. model.setState({"message": "Hello from the browser at " + t}); break; case "rmt": // Update the model with a remote timestamp via an Ajax request. model.setState("GET", "ajaxtest.php"); break; case "bad": // Simulate a failure by giving an invalid URL for the request. model.setState("GET", "xxxxxxxx.php"); break; case "dat": case "tmo": // Pass the mode to the server to test data or timeout problems. model.setState("GET", "ajaxtest.php?mode=" + mode); break; case "abt": // Tell the model to abort the current request if still running. model.abort(); break; case "pol": // Toggle the policy for how to handle Ajax request collisions. if (model.collpol == MVC.Connect.Ignore) { model.setCollisionPolicy(MVC.Connect.Change); alert("Collision policy has been toggled to "Change"."); } else { model.setCollisionPolicy(MVC.Connect.Ignore); alert("Collision policy has been toggled to "Ignore"."); } break; } }
To write your own Ajax application that uses MVC, derive your own
models from Model
and your own views
from View
. These are the prototype
objects that we used for TestModel
and TestView
previously. Next, let’s
look at the interfaces for each of these objects and explore their
implementations.
The Model
object is the
prototype object for all models. The public interface for Model
contains the methods for which a default
implementation is beneficial to most models. For example, the interface
provides default methods for initializing the model, setting and getting
the state of the model, subscribing and unsubscribing views, and
notifying views of state changes in the model:
init()
Initializes the model, which, by default, sets the state to an empty object and notifies the views for the first time. You can override this method to do something different to initialize your model.
setState(mixed, url,
post)
Sets the state of the model. If you pass only one
argument to the method (mixed
),
the state for the model is set to the object passed in mixed
. If you pass two arguments
(mixed
and url
), the state for the model is fetched
remotely using Ajax via the method in mixed
(GET
or POST
) and the URL you specify in
url
. If you pass three
arguments, the first must specify POST
. The state for the model is fetched
remotely via Ajax as in the case for two arguments, but the third
argument passes POST data in the same format as that accepted by
the YUI Connection Manager.
getState()
Returns whatever data has been stored previously by
setState
as the current state
of the model.
subscribe(view)
Inserts the view specified by view
into the list of views that will be
notified about changes to the model.
unsubscribe(view)
Deletes the view specified by view
from the list of views that will be
notified about changes to the model.
notify()
Notifies all views that are subscribed to the model
about changes to the model. This method is called automatically
within the default implementations of setState
and init
. Call this method whenever you need
to trigger notifications yourself (for example, in your own
implementations of init
or
setState
).
This section presents some of the implementation details for the
Model
prototype object. The Model
object is responsible for managing views
and handling updates from connections established by the YUI Connection
Manager. The implementation for Model
has one important method that we have not yet discussed:
Example 8-11
presents the complete implementation for Model
, including the default implementations
for the methods outlined earlier for the public interface of Model
.
// Place the Model object within its own namespace; create it if needed. if (!window.MVC) { MVC = {}; } MVC.Model = function() { MVC.Connect.call(this); this.state = {}; this.views = new Array(); }; // Model objects are derived from the Connect object (to handle Ajax). MVC.Model.prototype = new MVC.Connect(); MVC.Model.prototype.init = function() { // Set up an empty state and notify the views for the first time. this.state = {}; this.notify(); }; MVC.Model.prototype.setState = function(mixed, url, post) { switch (arguments.length) { case 1: // One argument means the state for the model should be set // to the local object passed in mixed. this.state = mixed; this.notify(); break; case 2: // Two arguments means set the state by fetching it remotely // using Ajax via the method in mixed (GET). this.connect(mixed, url); break; case 3: // Three arguments means set the state by fetching it remotely // using an Ajax POST; pass the POST data as the last argument. // If you do a GET with three arguments, the third is ignored. this.connect(mixed, url, post); break; } }; MVC.Model.prototype.getState = function() { return this.state; }; MVC.Model.prototype.update = function(o) { var r; // We're using JSON because the data stored as the state of the model // is an object. try { // This is where the response text is converted into a real object. r = json_parse(o.responseText); } catch(err) { // Handle if there is an issue creating the real JavaScript object. r = ""; } if (typeof r != "object") { // If we don't get an object as a response, treat it as a failure. this.recover(o); } else { // Store the state and notify the views only when we're successful. this.state = r; this.notify(); } }; MVC.Model.prototype.subscribe = function(view) { // Subscribe the view by inserting it into the list of subscribers. this.views.push(view); }; MVC.Model.prototype.unsubscribe = function(view) { var n = this.views.length; var t = new Array(); // Unsubscribe the view by removing it from the list of subscribers. for (var i = 0; i < n; i++) { if (this.views[i].id == view.id) t.push(this.views[i]); } this.views = t; }; MVC.Model.prototype.notify = function() { var n = this.views.length; // Notifying all views means to invoke the update method of each view. for (var i = 0; i < n; i++) { this.views[i].update(this); } };
The View
object is the
prototype object for all views. Its public interface consists of just
one method:
attach(m, i)
Attaches the model specified by m
to the view and subscribes the view to
it. The argument i
is the
id
attribute for the view’s
outermost div
. The id
attribute is stored with the view to
make it easy to pinpoint where to modify the DOM; the view just
needs to call document.getElementById
.
The abstract interface for View
consists of a single method that specific
views are expected to implement as needed:
The implementation details of View
focus on attaching models to views and
prescribing an interface by which views update themselves. Example 8-12 presents the
complete implementation for View
.
MVC.View = function() { this.model = null; this.id = null; }; MVC.View.prototype.attach = function(m, i) { // Make sure to unsubscribe from any model that is already attached. if (this.model != null) this.model.unsubscribe(this); this.model = m; this.id = i; // Subscribe to the current model to start getting its notifications. this.model.subscribe(this); }; MVC.View.prototype.update = function() { // The default for updating the view is to do nothing until a derived // view can provide more details about what it means to update itself. };
The Model
object
presented earlier was derived from the Connect
prototype object because it needed to
support requests via Ajax. The Connect
object is built on top of the YUI
Connection Manager. The public interface for Connect
provides default methods for making
and aborting Ajax requests, as well as setting various connection
options, such as the collision policy and timeouts:
connect(method, url,
post)
Establishes an asynchronous connection for making a
request via Ajax. The method
argument is the string GET
or
POST
. The url
argument is the destination for the
request. When method
is set to
POST
, pass the post data in
post
using the format accepted
by the YUI Connection Manager. When the request returns, Connect
calls the update
method.
abort()
Terminates an Ajax request that is already in
progress using this Connect
object.
setTimeout(value)
Sets the number of milliseconds to use as the
timeout when making Ajax requests with the Connect
object.
setCollisionPolicy(value)
Sets the policy for handling collisions between
concurrent requests. You can pass
either MVC.Connect.Ignore
or MVC.Connect.Change
for value
. With MVC.Connect.Ignore
, new Ajax
requests using the same Connect
object are simply discarded while a connection is in progress.
With MVC.Connect.Change
, a new
request replaces the one in progress.
The abstract interface for Connect
consists of methods that objects
derived from Connect
are expected to
implement as needed. These methods allow a specific instance of Connect
to define what should happen when
requests succeed, time out, or fail. The nice thing about the structure
of this object is that because the following are all methods of Connect
, you can use the this
reference inside each method to access
members of your object. This offers a great opportunity for better
encapsulation when managing Ajax requests:
update(o)
Called by Connect
after an Ajax request is successful. Implement this method in your
derived object in whatever way your application requires to handle
successful requests. The argument o
is the status object passed into
handlers by the YUI Connection Manager.
abandon(o)
Called by Connect
after an Ajax request exceeds its timeout. Implement this method
in your derived object in whatever way your application requires
to handle requests that have timed out. The argument o
is the status object passed into
handlers by the YUI Connection Manager. Example 8-13 does not
invoke this method on requests terminated explicitly by calling
abort
, but you can easily
modify the code to do so.
recover(o)
Called by Connect
after an Ajax request experiences a failure. Implement this method
in your derived object in whatever way your application requires
to handle requests that have failed. The argument o
is the status object passed into
handlers by the YUI Connection Manager.
The implementation details of Connect
focus on a number of tasks related to
managing Ajax connections and prescribing an interface for handling
various situations that can occur during the execution of an Ajax
request. Example 8-13
presents the complete implementation for Connect
.
MVC.Connect = function() { this.req = null; this.timeout = MVC.Connect.Timeout; this.collpol = MVC.Connect.Ignore; }; // Set up a default for timeouts with Ajax requests (in milliseconds). MVC.Connect.Timeout = 5000; // These are the possible values used for setting the collision policy. MVC.Connect.Ignore = 0; MVC.Connect.Change = 1; MVC.Connect.prototype.connect = function(method, url, post) { // Allow only one connection through the YUI Connection Manager at a // time. Handle collisions based on the setting for collision policy. if (this.req && YAHOO.util.Connect.isCallInProgress(this.req)) { if (this.collpol == MVC.Connect.Change) { this.abort(); } else { return; } } // Use this as a semaphore of sorts to keep the critical section as // small as possible (even though JavaScript doesn't have semaphores). this.req = {}; // This ensures access to the Connect (and derived object) instance in // the update, abandon, and recover methods. It generates a closure. var obj = this; function handleSuccess(o) { // Call the method implemented in the derived object for success. obj.update(o); obj.req = null; } function handleFailure(o) { if (o.status == -1) { // Call the method provided by the derived object for timeouts. obj.abandon(o); obj.req = null; } else { // Call the method provided by the derived object for failures. obj.recover(o); obj.req = null; } } // Set up the callback object to pass to the YUI Connection Manager. var callback = { success: handleSuccess, failure: handleFailure, timeout: this.timeout }; // Establish the Ajax connection through the YUI Connection Manager. if (arguments.length > 2) { this.req = YAHOO.util.Connect.asyncRequest ( method, url, callback, post ); } else { this.req = YAHOO.util.Connect.asyncRequest ( method, url, callback ); } }; MVC.Connect.prototype.abort = function() { if (this.req && YAHOO.util.Connect.isCallInProgress(this.req)) { YAHOO.util.Connect.abort(this.req); this.req = null; } }; MVC.Connect.prototype.setTimeout = function(value) { this.timeout = value; }; MVC.Connect.prototype.setCollisionPolicy = function(value) { this.collpol = value; }; MVC.Connect.prototype.update = function(o) { // The default for this method is to do nothing. A derived object must // define its own version to do something specific to the application. }; MVC.Connect.prototype.abandon = function(o) { // The default for this method is to do nothing. A derived object must // define its own version to do something specific to the application. }; MVC.Connect.prototype.recover = function(o) { // The default for this method is to do nothing. A derived object must // define its own version to do something specific to the application. };
Up to now, we have touched on the idea of a controller only briefly. This is because the main job of a controller is to respond to messages or events, and the simplest controllers are just the event handlers for HTML elements. The event handler sets a new value in the appropriate model, which in turn causes the appropriate views to be updated. This might look something like the following in HMTL:
<input type="button" value="Preview" onclick="myModel.setState(...);" />
On the other hand, if setting the state of the model is more than
a simple procedure, you can always implement a controller object. The
typical interface for controller objects is to provide a handleMessage
method that can call upon the
appropriate methods to handle messages in a nicely encapsulated
way:
YAHOO.util.Event.addListener ( element, "click", myController.handleMessage, MyController.SampleMessage, myController );
Here, myController
is an
instance of MyController
derived from
Controller
. MyController.SampleMessage
is class data
member (see Chapter 2) for the type of
message to handle. Class data members provide a good way to define
possible message types.
A good application of Ajax with MVC is to manage accordion lists. An accordion list is a list or table for which you can show or hide additional items under the main items displayed in the list. For example, Figure 8-2 shows a list of search results for cars that have good green ratings. Each car in the table can be expanded to show additional trims for the car by clicking on the View button. Once the list has been expanded, you can hide the extra items again by clicking the Hide button.
The reason that Ajax and MVC work well for this example is that there’s no need to load all the entries for each car in the expanded lists when the entire page loads. For a large list of cars, most of the extra entries will never be expanded. Ajax provides a good way to retrieve the expanded lists of entries only as you need them. MVC helps manage the changes that need to take place to show or hide the expanded lists for any of the cars.
The Green Search Results module defines one view and one model for
each car in the main set of results. These models and views work with
the items that must be loaded when each car is expanded. You embed the
JavaScript for instantiating the models and views for the module using
the get_js
method. The
JavaScript is embedded (as opposed to linked) because the module needs
to create it dynamically at runtime. The module also specifies a number
of JavaScript links using the get_js_linked
method.
This is the list of files to be linked for the page to ensure the rest
of the JavaScript works properly. Example 8-14 illustrates how
the module class encapsulates all of the pieces for this module. The
example also illustrates the onclick
handler in
get_content
, which sets
the expansion for each list in motion.
class GreenSearchResults extends Module { protected $items; public function __construct($page, $items) { parent::__construct($page); $this->items = $items; } public function get_css_linked() { ... } public function get_js_linked() { return array ( "yahoo-dom-event.js", "connection.js", "model-view-cont.js", "greencars.js" ); } public function get_js() { $count = count($this->items); // This sets up the JavaScript array GreenSearchResultsModel.CarIDs // to embed. This holds the car ID for each item in $this->items. // We need it in JavaScript too for access when initializing MVC. $js_ids_array = $this->get_js_ids_array(); return <<<EOD $js_ids_array var i; var GreenSearchResultsModel.MReg = new Array(); var GreenSearchResultsModel.VReg = new Array(); for (i = 0; i < $count; i++) { // Instantiate a model for the item and set the associated car ID. // Store this in a static member of the model that holds all models. GreenSearchResultsModel.MReg[i] = new GreenSearchResultsModel( GreenSearchResultsModel.CarIDs[i]); // Instantiate a view for the item and set its position in the list. // Store this in a static member of the model that holds all views. GreenSearchResultsModel.VReg[i] = new GreenSearchResultsView(i); // Attach the model and view and set the ID of the expansion button. GreenSearchResultsModel.VReg[i].attach(GreenSearchResultsModel.MReg [i], "gsrexpbtn" + i); // Initialize the model for the item. GreenSearchResultsModel.MReg[i].init(); } EOD; } public function get_content() { $count = count($this->items); $rows = ""; for ($i = 0; $i < $count; $i++) { $exp_link = <<<EOD <img src="http://.../view.gif" onclick="GreenSearchResultsModel.VReg [$i].show();" /> EOD; $main = $this->get_row($i, $exp_link); $rows .= $main; } $header = $this->get_header(); return <<<EOD $header <table> $rows </table> EOD; } protected function get_header() { // Return the HTML markup for the header region of the module. ... } protected function get_row($i, $exp_link) { // Return the HTML markup for a single car row at position $i. ... } protected function get_js_ids_array() { // Return all car IDs from $this->items as a JavaScript array. ... } }
Example 8-15
presents the model and view objects, GreenSearchResultsModel
and GreenSearchResultsView
, that manage the
accordion lists for this example. Whenever you click the View button,
the event handler for the button click calls GreenSearchResultsView.show
. This method calls
setState
for the model, which makes
an Ajax request to get the expanded data for the car. Once the Ajax
request returns, the model calls its notify
method, which then invokes the update
method for the view. The update
method modifies the DOM to display the
expanded list based on the data in the model.
GreenSearchResultsModel = function(id) { MVC.Model.call(this); this.carID = id; }; GreenSearchResultsModel.prototype = new MVC.Model(); GreenSearchResultsModel.prototype.recover = function() { alert("Could not retrieve the cars you are trying to view."); }; GreenSearchResultsModel.prototype.abandon = function() { alert("Timed out fetching the cars you are trying to view."); }; GreenSearchResultsView = function(i) { MVC.View.call(this); // The position of the view is helpful when performing DOM updates. this.pos = i; } GreenSearchResultsView.prototype = new MVC.View(); GreenSearchResultsView.prototype.update = function() { var cars = this.model.state.cars; // There is no need to update the view or show a button for one car. if (this.total == 1) return; if (!cars) { // When no cars are loaded, we're rendering for the first time. // In this case, we likely need to do different things in the DOM. ... } else { // When there are cars loaded, update the view by working with // the DOM to show the cars that are related to the main car. ... } }; GreenSearchResultsView.prototype.show = function() { // When we show the view, make an Ajax request to get related cars. // This causes the view to be notified and its update method to run. this.model.setState("GET", "...?carid=" + this.model.carID); }; GreenSearchResultsView.prototype.hide = function() { // When we hide the view, modify the DOM to make the view disappear. ... };
In Chapter 9, we’ll discuss how to add caching to this implementation so that once an expanded list is retrieved, it doesn’t have to be fetched again when the View and Hide buttons for one car are clicked repeatedly.