Previous Page
Next Page

10.4. Improving Viewing with AJAX

Because the biggest problem with the viewer is that it's reloading content that's not changing, we want to look at AJAX patterns that we can use to avoid this. We have a number of options, but they all end up having the same effect: Instead of treating the viewer as one monolithic page, we get to treat it as a number of sub-pages. This allows us to load data for each section independently, which will keep us from needing to reload the slow graph when we just want to view the next month's data. This independent loading gives us a speed advantage over a monolithic approach. We end up with three different sections, one for each of the main elements. (These sections are shown in Figure 10-6.) Each section can be updated independently from the others, and in this case, input from the form section controls what the other sections do. Any change to the form, either in the selection of a different city or in the changing of the month, causes the data section to update. The graph will only be updated when the selected cities are changed.

Figure 10-6. Viewer with sections labeled

Sectioning a page is an easy AJAX pattern to implement, but it's not without its disadvantages. The biggest disadvantage is that the actions you perform are no longer directly bookmarkable. You can add a bookmark link that always contains the URL to get to your current view, but users might not recognize it and will just bookmark the page like normal, getting its default value instead of the current one. There are some solutions to this problem. They involve using the fragment part of the URL (the value after the #), but they suffer from browser compatibility problems and complex programming models. The fragment solutions also have a hard time fixing both the Back and Forward buttons and booking at the same time. Eventually, there will be an easy solution to the bookmark problem, but it's not ready today.

10.4.1. Viewer HTML Updated for AJAX

To implement our AJAX actions, we start by adding a submit handler to the form:

<form action="<?php echo $action; ?>" onsubmit="return updatePage(this)" id="form">

This function returns false, canceling the normal form submission process. Now that we have client-side code in control of the submission process, we can figure out what needs to be updated. This means we need some code to always update the data section and to update the graph section only when the selected cities have changed. Using HTML_AJAX, updating the data section is simple, as shown here:

var queryString = HTML_AJAX.formEncode(form);
var callback = function(result) {

    document.getElementById('table').innerHTML = result;


We start the process by using the HTML_AJAX.formEncode method to turn the values of the form into a queryString. This string is the same one that you can see after the "?" in the URL when doing a normal form submission. Then, we build a callback function that will be called when the HTTP request to the server is complete. We then call HTML_AJAX.grab against the AjaxTable.php page, passing in the queryString we built from the form and the callback we just built. The new PHP script will be explained later in section 10.4.2 (Viewer PHP Script Updated for AJAX), but it follows the same subpage split, generating just the data table section.

Figuring out if the currently selected cities have changed is more difficult. The basis of this process is a function that looks at the form and creates a hash of its current check status. This function will run on page load and then again each time we detect a change so that we have a value to compare against the next time. The code for this function is shown here:

function updateCurrentCities() {
  var els = document.getElementById('form').elements;
  for(var i = 0; i < els.length; i++) {
   if (els[i].name == 'cities[]') {
     cities[els[i].value] = els[i].checked;

The cities variable is defined outside of this function as var cities = {};, which puts it in the global scope and lets other functions use it. The updateCurrentCities function works by first getting all the elements of the form and then looping over them to look for cities elements. Because each checkbox has the same name (cities[]), we can use those elements that are checkboxes for city selection; if the elements had different names, an easy solution would be to give them all the same CSS class. The value of each checkbox is the name of the city, so we use that as the key in the cities hash and then store if the box is checked as the value.

In our form-handling function, we'll loop over the elements again, comparing each value against the value stored in the hash. If any one of the checkboxes doesn't match, we stop the comparison, update the src property of the graph, causing it to reload, and then run updateCurrentCities again. The code that does this is shown here:

// check whether the cities have changed (graph reload)
var els = form.elements;
for(var i = 0; i < els.length; i++) {
  if (els[i].name == 'cities[]' &&
    cities[els[i].value] != els[i].checked) {
    document.getElementById('graph').src =

Now that we have the basic AJAX functionality added to the page, we are ready to start improving its usability. The first step is adding a link that can be bookmarked and will return you to the current set of data you're viewing. Upon page load, this link will contain the same URL as the form, so we can use the $action PHP variable again:

<a id="blink" href="<?php echo $action;?>">Bookmarkable Link</a>

Once the link has been added to the page, we need to add code to update it when the form is submitted. This code is pretty simple: It checks the current HRef on the blink tag to see if it has a "?" in it. If it does, all the content after it is removed. Then, we append the new queryString to the link. This is the same string we send to the AjaxTable.php script. The code that performs this bookmark link updating is shown here:

// update bookmarkable link
var blink = document.getElementById('blink');
if (blink.href.indexOf('?') > 0) {
     blink.href = blink.href.substring(
blink.href = blink.href+'?'+queryString;

Adding in the bookmark link helps solve some of the usability problems caused by AJAX, but it doesn't do anything for the page's original usability problem, which is poor feedback. To do this, we need to add a feedback mechanism to the loading of the graph. The basic way this works is that we create a DIV element and position it over the graph. It will contain the message "Loading, please wait" and can be further formatted using CSS to make it look nice. This DIV will be hidden by an onload handler that we will add to the image. To improve the look of this loading notice, we'll use some visual effects from the scriptaculous library. These steps are accomplished by using the startLoad function, which is shown in Listing 10-9. The startLoad function takes as its single parameter the element to which to add the loading effect.

Listing 10-9. The startLoad JavaScript Function

1 function startLoad(element) {
2     if (!element.loading) {
3         element.loading = document.createElement('div');
4         element.loading.className='loading';
5         element.loading.innerHTML='Loading, please wait';
6 =
7             element.clientHeight+'px';
8   element.parentNode.appendChild(element.loading);
9         element.onload = function() {
10             new Effect.Fade(this.loading);
11         }
12     }
13     else {
14         new Effect.Appear(element.loading);
15     }
16  }

In basic operation, the startLoad function takes the loading div and shows it using the Appear effect (line 14); it then sets up a Fade effect to hide the div when the page is loaded (lines 911). The rest of the function (lines 28) handles the initial case where we dynamically create the loading div. As we create the div, we assign it to a property on the element to which we're adding the message. Doing this makes the code reusable and helps make it clear to what element the loading div belongs. Line 3 creates the div, line 4 sets its CSS class and lets it be styled, and line 5 sets the height of the element. Width can be easily set in CSS because a width of 100% works, but percentage heights don't work in IE, so we need to calculate the absolute height that is needed to cover the area where the element will be loaded. The setup finishes by appending the loading node to the parent of the element to which it will be applied. This means that the image tag for the graph must be created inside another element, but this works fine because we will want that wrapper div for CSS purposes.

The final usability change to make is to replace the default HTML_AJAX loading feedback effect with a custom one. Because updating the data table is normally a fast process, we want to highlight the table when it has been updated. Without this visual queue, users might not notice that something has changed. We'll also change the cursor to the standard page load progress cursor while the table is updating; that way, the user will get feedback if the loading is delayed by a slow network connection. This is implemented by creating an options hash and passing it to HTML_AJAX.grab:

var options = {
  Open: function() {
 = 'progress';
  Load: function() { = 'default';
    new Effect.Highlight('table');

This options hash creates a custom event handler that is called when a connection to the server is opened, and this handler sets the cursor to progress. Then we create a custom function to be called when the page load is finished. This function sets the cursor back to default and then runs a highlight effect on the table. This effect highlights the table in yellow and then fades out the highlight over a short period of time.

10.4.2. Viewer PHP Script Updated for AJAX

Now that we've looked at the AJAX changes we will be adding, let's look at what other changes need to be made to build an AJAX version of the viewer. The first step is to break the Standard.php file into three different files. The first 34 lines of the file are pulled out into a file called AjaxSetup.php. This allows the basic setup code and form value loading to be easily done in multiple files. Lines 71101 of Listing 10-8 are also pulled out into their own file, AjaxTable.php. At the top of this new file, we require AjaxSetup.php, giving it access to the same variables that were used while the code was still in one file. This file now contains the code used to create the data table section. Finishing up the sectioning, Standard.php is renamed to Ajax.php so that we can compare the two versions, and the removed code is added back into the page using the require function so that the initial page generation happens in the same way. Neither the backend class nor the Graph.php script needs to change. Listing 10-10 shows the updated AJAX code.

Listing 10-10. AjaxSetup.php

1  <?php
2  // set up the data instance
4  // include data class
5  require_once 'SunRiseSet.class.php';
6  $data = new SunRiseSet();
8  // defaults
9  $month = 1;
10 $cities = array('PHOENIX, ARIZONA');
12 // load options
13 if (isset($_GET['month'])) {
14  $month = (int)$_GET['month'];
15 }
17 if (isset($_GET['cities'])) {
18  $cities = (array)$_GET['cities'];
19 }
21 // Set the selected options on the data class
22 $data->setMonth($month);
23 $data->setCities($cities);
25 // build the graph query string
26 $graphOptions = '';
27 foreach($cities as $city) {
28  $graphOptions .= "cities[]=$city&";
29 }
31 $action = htmlentities($_SERVER['PHP_SELF']);
32 $months = $data->monthList();
33 ?>

The AjaxSetup.php file contains the first 34 lines of Standard.php. Because it's in a standalone file, it can be used in both Ajax.php and AjaxTable.php, performing the setup duties for each of them. The AjaxTable.php file will generate the table data and is shown in Listing 10-7, while Ajax.php, shown in Listing 10-1, will be the main entry point for the viewer, creating the main UI and the AJAX code for controlling the viewer.

Listing 10-11. AjaxTable.php

1  <?php
2  // build just the data table for the viewer
3  require_once 'AjaxSetup.php';
4  ?>
5  <b><?php echo $months[$month]; ?></b>
6  <table cellpadding="2" cellspacing="0" border="1">
7  <thead>
8    <tr>
9      <th rowspan="2">Day</th>
10   <?php foreach($cities as $city) { ?>
11     <th colspan="2"><?php echo $city; ?></th>
12  <?php } ?>
13  </tr>
14  <tr>
15  <?php foreach($cities as $city) { ?>
16    <th>Rise</th>
17    <th>Set</th>
18  <?php } ?>
19  </tr>
20 </thead>
21 <tbody>
22 <?php foreach($data->monthsData() as $row) { ?>
23   <tr>
24     <td><?php echo $row[0]['day']; ?></td>
25   <?php foreach($row as $city) { ?>
26     <td><?php echo $city['rise']; ?></td>
27     <td><?php echo $city['set']; ?></td>
28   <?php } ?>
29   </tr>
30 <?php } ?>
31 </tbody>
32 </table>

AjaxTable.php contains lines 71101 of Standard.php (see Listing 10-13). The only change to this code is the addition of the require_once of AjaxSetup.php, at the top. This allows AjaxTable.php to be called directly. Because we use require_once to include AjaxSetup.php, it will be included only once during a page load. This lets Ajax.php require AjaxSetup.php and AjaxTable.php without worrying about the setup code running multiple times.

Listing 10-12. Ajax.php

1 <?php
2   // AJAX version of a sun rise/set viewer
3   require_once 'AjaxSetup.php';
4   ?>
5   <html>
6   <head>
7       <title>AJAX Sun Rise and Set Viewer</title>
8   <script src="scriptaculous/prototype.js"
9       type="text/javascript"></script>
10  <script src="scriptaculous/scriptaculous.js"
11      type="text/javascript"></script>
12  <script src="server.php?client=all"
13     type="text/javascript"></script>

In comparison to Standard.php, the setup for this page has changed quite a bit. All the PHP setup is now handled in AjaxSetup.php, and on the HTML side, we're now including a couple of JavaScript libraries. On line 3, we include the AjaxSetup.php file, and we use the require_once function to make sure the file gets pulled in only one time. On lines 811, we include the scriptaculous library; this library will be used for a number of visual effects. The setup completes on lines 1213, including the HTML_AJAX JavaScript library. The PHP script, Server.php, is identical to the one shown in Listing 9-1.

Listing 10-13. Ajax.php Continued

15 <script type="text/javascript">
16  var cities = {};
18  function updatePage(form) {
19      var queryString = HTML_AJAX.formEncode(form);
21     // check if the cities have changed (graph reload)
22     var els = form.elements;
23     for(var i = 0; i < els.length; i++) {
24         if (els[i].name == 'cities[]'
25         && cities[els[i].value] != els[i].checked) {
26             startLoad(document.getElementById('graph'));
27             document.getElementById('graph').src =
28                 'Graph.php?'+queryString;
29             updateCurrentCities();
30           break;
31         }
32     }
34     // when the AJAX request is done, we replace the
35     // current table with a new one
36     var callback = function(result) {
37         document.getElementById('table').innerHTML
38          = result;
39     }
41     // change the cursor to indicate loading
42     var options = {
43         Open: function() {
44    = 'progress';
45         },
46         Load: function() {
47    = 'default';
48             new Effect.Highlight('table');
49         }
50     }
52     // make the ajax request
53     HTML_AJAX.grab('AjaxTable.php?'+queryString,
54         callback,options);
56     // update bookmarkable link
57     var blink = document.getElementById('blink');
58     if (blink.href.indexOf('?') > 0) {
59         blink.href = blink.href.substring(
60             0,blink.href.indexOf('?'));
61     }
62     blink.href = blink.href+'?'+queryString;
64     return false;
65   }
67  // update the cities, marking which ones are checked
68  function updateCurrentCities() {
69      var els = document.getElementById('form').elements;
70      for(var i = 0; i < els.length; i++) {
71          if (els[i].name == 'cities[]') {
72              cities[els[i].value] = els[i].checked;
73          }
74      }
75  }
77  // show the loading effect on the image
78  function startLoad(elem) {
79      if (!elem.loading) {
80          elem.loading = document.createElement('div');
81          elem.loading.className= 'loading';

82          elem.loading.innerHTML= 'Loading, please wait';
83 =
84              elem.clientHeight+'px';
85          elem.parentNode.appendChild(element.loading);
86          elem.onload = function() {
87              new Effect.Fade(this.loading);
88          }
89      }
90      else {
91          new Effect.Appear(elem.loading);
92      }
93  }
94  </script>

Lines 1594 contain the JavaScript needed for the viewer. Its different components have already been explained; they provide AJAX loading of page sections and various feedback effects. Line 16 defines the cities hash; it is used to see if the selected cities to view have changed. Lines 1865 define the updatePage function; this function is called by the onsubmit handler on the form and takes that form as its parameter. Line 19 processes the form into a query string. Then, lines 2132 check whether any of the cities have changed and reload the graph as needed. Lines 3454 set up the AJAX call and then make the call to update the data table. In this area, lines 3639 set up the callback, lines 4150 define the feedback options, and lines 5354 make the AJAX request using HTML_AJAX.grab. The updatePage function finishes by updating the bookmark link (lines 5762). Lines 6875 define the updateCurrentCities function, which populates the cities hash. The JavaScript section finishes with the startLoad function on lines 7893, which handles creating and showing the graph-loading message.

Listing 10-14. Ajax.php Continued

95  <style type="text/css">
96  .loadable {
97      position: relative;
98  }
99  .loading {
100     position: absolute;
101     background-color: #eee;
102     text-align: center;
103     top: 0;
104     left: 0;
105     width: 100%;
106 }

107  #table {
108      float: left;
109 }
110 </style>

Lines 95110 (Listing 10-14) contain some CSS rules that style the elements used in the loading effects. The .loadable rule on lines 9698 is applied to the container outside the element that will be loaded; making its position relative allows it to be the parent element for the absolutely positioned loading DIV. Lines 99106 contain the rules for the loading DIV; they position it absolutely to cover the entire graph and give it a gray background. The CSS rules finish up with a rule (lines 109110) that floats the table to the left. This rule is needed to keep the highlight effect from bleeding out onto the rest of the page, and it is needed only because we're using floats on other parts of the page.

Listing 10-15. Ajax.php Continued

111 </head>
112 <body onload="updateCurrentCities()">
113 <div style="float:left; width: 610px;">
115 <div class="loadable">
116 <img src="Graph.php?<?php echo $graphOptions; ?>"
117     id="graph" width="600" height="400"
118     alt="sun rise and set">
119 </div>
121 <script type="text/javascript">
122 startLoad(document.getElementById('graph'));
123 </script>
125 <form action="<?php echo $action; ?>" id="form"
126    onsubmit="return updatePage(this)">
127 <fieldset>
128    <legend>Cities</legend>
129    <?php foreach($data->possibleCities()
130        as $city) { ?>
131    <label><?php echo $city; ?>
132        <input type="checkbox" name="cities[]"
133        value="<?php echo $city; ?>"
134        <?php if(in_array($city,$cities)) {
135             echo "CHECKED";
136        } ?>>
137    </label>
139   <?php } ?>
140 </fieldset>
141 <label>View Month:
142 <select name="month">
143 <?php foreach($months as $key => $m) { ?>
144     <option value="<?php echo $key;?>"
145     <?php if($key == $month) { echo 'SELECTED'; } ?>>
146         <?php echo $m; ?></option>
147 <?php } ?>
148 </select>
149 </label>
150 <input type="submit" value="Update View">
151 <a id="blink" href="<?php echo $action; ?>"
152     >Bookmarkable Link</a>
153 </form>
154 </div>
156 <div id="table">
157 <?php include 'AjaxTable.php'; ?>
158 </div>
159 </body>
160 </html>

Little has changed in the rest of the document when compared to the non-AJAX version. Line 112 contains an onload call to updateCurrentCities; this populates which cities are selected at the time of the page load. The graph output on lines 115123 also has a number of changes from the standard version. The graph now has an ID so that its src value can be changed later, and its container div has the loadable class applied to it. Finishing up the graph code is a call to startLoad to pass in the graph; this provides a loading message on the initial page load.

Lines 125153 contain the form code; the form contains several small changes. The form definition now contains an ID and an onsubmit property that calls pageUpdate (lines 125126). It also has a bookmark link added to it (lines 151152). On page load, this link is the same value as the form submit and is updated by the pageUpdate JavaScript function as AJAX requests are made. Finishing up the document, we include AjaxTable.php, which renders the data table.

Previous Page
Next Page