Chapter 8. Large-Scale Ajax

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.

In the Browser

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.

Managing Connections

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.

Note

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.

Example 8-1. A simple Ajax request
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.

Example 8-2. A simple JSON response
<?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.

Using Ajax Libraries

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.

Ajax with Dojo

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:

GET

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.
      }
   }
);
POST

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.
      }
   }
);

Ajax with jQuery

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:

GET

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.
      }
   }
);
POST

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.
      }
   }
);

Ajax with Prototype

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:

GET

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.
      }
   }
);
POST

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.
      }
   }
);

Ajax with YUI

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.

Note

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.

GET

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,
         ...
      }
   }
);
POST

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..."
);

On the Server

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.

Exchange Formats

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.

Plain text

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.

Example 8-3. Returning an Ajax response in plain text using 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);
?>

XML

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.

Example 8-4. Returning an Ajax response in XML using PHP
<?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);
?>

JSON

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.

Example 8-5. Returning an Ajax response in JSON using PHP
<?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);
?>

Server Proxies

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.

Example 8-6. A server proxy for Ajax requests
<?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);
?>

Modular Ajax

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.

Example 8-7. Handling an Ajax request with a data manager
<?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);
?>

MVC and Ajax

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.

Using Ajax with MVC

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.

An application for testing simple Ajax requests managed using MVC
Figure 8-1. An application for testing simple Ajax requests managed using MVC
Local

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.

Remote

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.

Fail

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.

Data

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.

Timeout

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.

Abort

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.

Policy

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.

Example 8-8. HTML for the Ajax example
<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.

Example 8-9. The model and views for the Ajax example
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.

Example 8-10. Using the objects from Example 8-9
// 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.

Public Interface for the Model Object

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

Implementation of the Model Object

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:

update(o)

This is different from the update method for subscribers called within notify. This method is the callback that the YUI Connection Manager calls when the connection from setState returns. The argument o is the status object passed into handlers by the YUI Connection Manager.

Example 8-11 presents the complete implementation for Model, including the default implementations for the methods outlined earlier for the public interface of Model.

Example 8-11. The Model prototype object for Ajax with MVC
// 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);
   }
};

Public Interface for the View Object

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.

Abstract Interface for the View Object

The abstract interface for View consists of a single method that specific views are expected to implement as needed:

update()

Called by the Model object within its notify method to give a view the chance to update itself based on a state change in the model. Implement this method to perform whatever updates are needed in your application based on a change to the model.

View Object Implementation

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.

Example 8-12. The View prototype object for Ajax with MVC
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.
};

Public Interface for the Connect Object

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.

Abstract Interface for the Connect Object

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.

Implementation of the Connect Object

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.

Example 8-13. The Connect prototype object for Ajax with MVC
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.
};

Controllers

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.

An Example of Ajax with MVC: Accordion Lists

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 Green Search Results module with an accordion list
Figure 8-2. The Green Search Results module with an accordion list

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.

Example 8-14. A class for the Green Search Results module with an accordion list
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.

Example 8-15. The model and view objects for the accordion list
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.

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

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