Tuesday, July 14, 2009

AJAX and PHP Building Responsive Web Applications by Cristian Darie, Bogdan Brinzarea Chapter 8

AJAX Grid

Data grids have always been one of the areas where web applications have had a serious disadvantage compared to desktop programs. The fact that the page needed a full reload when switching between grid pages, or when updating grid details, harmed the application from a usability point of view. Technically, fully reloading the page has bad effects as well, unnecessarily wasting network resources.

But now you know there is a smarter solution to this problem. You can use AJAX to update the grid content without refreshing the page. You can keep your beautiful design in the client browser without even one page blink. Only the table data is refreshed, not the whole page.

The novelty in this chapter is that we'll use Extensible Stylesheet Language Transformation (XSLT) and XML Path Language (XPath) to generate the client output. XSLT and XPath are part of the Extensible Stylesheet Language (XSL) family. XSLT allows defining rules to transform an XML document to another format and XPath is a very powerful query language that allows performing searches and retrieving data from XML documents. When used to create web front ends, XSLT permits implementing a very flexible architecture, in which the server outputs the data in XML format, and that data is transformed to HTML using an XSL transformation. You can find an introduction to XSL in Appendix C at http://ajaxphp.packtpub.com, and a good description at http://en.wikipedia.org/wiki/Extensible_Stylesheet_Language.

Note the XSL transformation can be applied at both client side and server side. The implementation in this chapter relies on client functionality to perform the transformation. This doesn't require any special features of the server, but it poses some constraints for the client. In Chapter 9, you will see how to apply the transformation at the server side using PHP functionality, in which case you require this feature to be enabled in PHP, but the solution works with any client, as the client receives directly the HTML code it is supposed to display.

In this chapter, you'll use:

• XSL to generate an HTML data grid based on XML data received from the server.
• AJAX to implement the editable data grid. The user should be able to switch between product pages and edit product details without experiencing any page reloads.

Implementing the AJAX Grid Using Client-Side
XSLT
In this case study, you will build an AJAX-enabled editable data grid. The products used to populate the grid were kindly provided to us by http://www.the-joke-shop.com/.

Figure 8.1 shows the second page of products and Figure 8.2 shows how the grid looks after the
Edit link is clicked, and one of the products enters edit mode.

Figure 8.1: AJAX Grid in Action

Figure 8.2: AJAX Grid in Edit Mode

Because there's a lot of dynamically output data to generate, this is a good opportunity to learn about XSLT.

Let's first write the code so you'll have a working solution, and then we will comment upon it. The program will be composed of the following files:

• grid.php
• grid.class.php
• error_handler.php
• config.php
• grid.css
• index.html
• grid.xsl
• grid.js

Time for Action—AJAX Grid
1. Let's start by preparing the database for this exercise. We basically need a table with
products. You can either execute the SQL script product.sql from the code download, or you can type it (the code snippet below creates only the first 10 products; please use the code download for the complete list of products):

CREATE TABLE product
(
product_id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL DEFAULT '',
price DECIMAL(10,2) NOT NULL DEFAULT '0.00',
on_promotion TINYINT NOT NULL DEFAULT '0', PRIMARY KEY (product_id)
);

INSERT INTO product(name, price, on_promotion) VALUES('Santa Costume', 14.99, 0);
INSERT INTO product(name, price, on_promotion) VALUES('Medieval Lady', 49.99, 1);
INSERT INTO product(name, price, on_promotion)
VALUES('Caveman', 12.99, 0);
INSERT INTO product(name, price, on_promotion) VALUES('Costume Ghoul', 18.99, 0);
INSERT INTO product(name, price, on_promotion)
VALUES('Ninja', 15.99, 0);
INSERT INTO product(name, price, on_promotion) VALUES('Monk', 13.99, 0);
INSERT INTO product(name, price, on_promotion) VALUES('Elvis Black Costume', 35.99, 0);
INSERT INTO product(name, price, on_promotion)
VALUES('Robin Hood', 18.99, 0); INSERT INTO product(name, price, on_promotion)
VALUES('Pierot Clown', 22.99, 1);
INSERT INTO product(name, price, on_promotion) VALUES('Austin Powers', 49.99, 0);

2. Create a new subfolder called grid under your ajax folder.
3. We'll start writing the code with the server side. In the grid folder, create a new file called grid.php, which will respond to client's asynchronous requests:
<?php
// load error handling script and the Grid class
require_once('error_handler.php');
require_once('grid.class.php');
// the 'action' parameter should be FEED_GRID_PAGE or UPDATE_ROW
if (!isset($_GET['action']))
{
echo 'Server error: client command missing.';
exit;
}
else
{
// store the action to be performed in the $action variable
$action = $_GET['action'];
}
// create Grid instance
$grid = new Grid($action);
// valid action values are FEED_GRID_PAGE and UPDATE_ROW
if ($action == 'FEED_GRID_PAGE')
{
// retrieve the page number
$page = $_GET['page'];
// read the products on the page
$grid->readPage($page);
}
else if ($action == 'UPDATE_ROW')
{
// retrieve parameters
$id = $_GET['id'];
$on_promotion = $_GET['on_promotion'];
$price = $_GET['price'];

$name = $_GET['name'];
// update the record
$grid->updateRecord($id, $on_promotion, $price, $name);
}
else
echo 'Server error: client command unrecognized.';
// clear the output if(ob_get_length()) ob_clean();
// headers are sent to prevent browsers from caching
header('Expires: Fri, 25 Dec 1980 00:00:00 GMT'); // time in the past header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . 'GMT');
header('Cache-Control: no-cache, must-revalidate');
header('Pragma: no-cache');
header('Content-Type: text/xml');
// generate the output in XML format header('Content-type: text/xml');
echo '<?xml version="1.0" encoding="ISO-8859-1"?>';
echo '<data>';
echo '<action>' . $action . '</action>';
echo $grid->getParamsXML();
echo $grid->getGridXML();
echo '</data>';
?>

4. Create a new file called grid.class.php, and add the following code to it:
<?php
// load configuration file
require_once('config.php');
// start session session_start();

// includes functionality to manipulate the products list class Grid
{
// grid pages count public $mTotalPages;
// grid items count public $mItemsCount;
// index of page to be returned
public $mReturnedPage;
// database handler private $mMysqli;
// database handler private $grid;

// class constructor function __construct()
{
// create the MySQL connection
$this->mMysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD, DB_DATABASE);
// call countAllRecords to get the number of grid records
$this->mItemsCount = $this->countAllRecords();
}

// class destructor, closes database connection function __destruct()
{
$this->mMysqli->close();
}
// read a page of products and save it to $this->grid public function readPage($page)
{
// create the SQL query that returns a page of products
$queryString = $this->createSubpageQuery('SELECT * FROM product',
$page);

// execute the query
if ($result = $this->mMysqli->query($queryString))
{
// fetch associative array
while ($row = $result->fetch_assoc())
{
// build the XML structure containing products
$this->grid .= '<row>';
foreach($row as $name=>$val)
$this->grid .= '<' . $name . '>' . htmlentities($val) .
'</' . $name . '>';
$this->grid .= '</row>';
}
// close the results stream
$result->close();
}
}

// update a product
public function updateRecord($id, $on_promotion, $price, $name)
{
// escape input data for safely using it in SQL statements
$id = $this->mMysqli->real_escape_string($id);
$on_promotion = $this->mMysqli->real_escape_string($on_promotion);
$price = $this->mMysqli->real_escape_string($price);
$name = $this->mMysqli->real_escape_string($name);
// build the SQL query that updates a product record
$queryString = 'UPDATE product SET name="' . $name . '", ' .
'price=' . $price . ',' .
'on_promotion=' . $on_promotion .
' WHERE product_id=' . $id;
// execute the SQL command
$this->mMysqli->query($queryString);
}

// returns data about the current request (number of grid pages, etc)
public function getParamsXML()
{
// calculate the previous page number
$previous_page =
($this->mReturnedPage == 1) ? '' : $this->mReturnedPage-1;
// calculate the next page number
$next_page = ($this->mTotalPages == $this->mReturnedPage) ?
'' : $this->mReturnedPage + 1;
// return the parameters return '<params>' .
'<returned_page>' . $this->mReturnedPage . '</returned_page>'.
'<total_pages>' . $this->mTotalPages . '</total_pages>'.
'<items_count>' . $this->mItemsCount . '</items_count>'.
'<previous_page>' . $previous_page . '</previous_page>'.
'<next_page>' . $next_page . '</next_page>' .
'</params>';
}

// returns the current grid page in XML format public function getGridXML()
{
return '<grid>' . $this->grid . '</grid>';
}

// returns the total number of records for the grid private function countAllRecords()
{
/* if the record count isn't already cached in the session,
read the value from the database */

if (!isset($_SESSION['record_count']))
{
// the query that returns the record count
$count_query = 'SELECT COUNT(*) FROM product';
// execute the query and fetch the result
if ($result = $this->mMysqli->query($count_query))
{
// retrieve the first returned row
$row = $result->fetch_row();
/* retrieve the first column of the first row (it represents the records count that we were looking for), and save its value in
the session */
$_SESSION['record_count'] = $row[0];
// close the database handle
$result->close();
}
}
// read the record count from the session and return it return $_SESSION['record_count'];
}

// receives a SELECT query that returns all products and modifies it
// to return only a page of products
private function createSubpageQuery($queryString, $pageNo)
{
// if we have few products then we don't implement pagination
if ($this->mItemsCount <= ROWS_PER_VIEW)
{
$pageNo = 1;
$this->mTotalPages = 1;
}
// else we calculate number of pages and build new SELECT query
else
{
$this->mTotalPages = ceil($this->mItemsCount / ROWS_PER_VIEW);
$start_page = ($pageNo - 1) * ROWS_PER_VIEW;
$queryString .= ' LIMIT ' . $start_page . ',' . ROWS_PER_VIEW;
}
// save the number of the returned page
$this->mReturnedPage = $pageNo;
// returns the new query string
return $queryString;
}
// end class Grid
}
?>

5. Add the configuration file, config.php:
<?php
// defines database connection data define('DB_HOST', 'localhost');
define('DB_USER', 'ajaxuser'); define('DB_PASSWORD', 'practical'); define('DB_DATABASE', 'ajax');
// defines the number of visible rows in grid define('ROWS_PER_VIEW', 10);
?>

6. Create the error-handling script, error_handler.php with the following contents:
<?php
// set the user error handler method to be error_handler set_error_handler('error_handler', E_ALL);
// error handler function
function error_handler($errNo, $errStr, $errFile, $errLine)
{

// clear any output that has already been generated ob_clean();
// output the error message
$error_message = 'ERRNO: ' . $errNo . chr(10) .
'TEXT: ' . $errStr . chr(10) .
'LOCATION: ' . $errFile .
', line ' . $errLine;
echo $error_message;
// prevent processing any more PHP scripts exit;
}
?>

7. It's time for the client now. Start by creating index.html:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<title>AJAX Grid</title>
<script type="text/javascript" src="grid.js"></script>
<link href="grid.css" type="text/css" rel="stylesheet"/>
</head>
<body onload="init();">
<div id="gridDiv" />
</body>
</html>

8. Now let's create the XSLT file named grid.xsl that will be used in the JavaScript code to generate the output:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<h2>AJAX Grid</h2>
<xsl:call-template name="menu"/>
<form id="grid_form_id">
<table>
<tr>
<th>ID</th>
<th>Name</th>
<th>Price</th>
<th>Promo</th>
<th></th>
</tr>
<xsl:for-each select="data/grid/row">
<xsl:element name="tr">
<xsl:attribute name="id">
<xsl:value-of select="product_id" />
</xsl:attribute>
<td><xsl:value-of select="product_id" /></td>
<td><xsl:value-of select="name" /> </td>
<td><xsl:value-of select="price" /></td>
<td>
<xsl:choose>
<xsl:when test="on_promotion &gt; 0">
<input type="checkbox" name="on_promotion"
disabled="disabled" checked="checked"/>
</xsl:when>
<xsl:otherwise>
<input type="checkbox" name="on_promotion" disabled="disabled"/>
</xsl:otherwise>
</xsl:choose>
</td>

<td>
<xsl:element name="a">
<xsl:attribute name = "href">#</xsl:attribute>
<xsl:attribute name = "onclick">
editId(<xsl:value-of select="product_id" />, true)
</xsl:attribute> Edit
</xsl:element>
</td>
</xsl:element>
</xsl:for-each>
</table>
</form>
<xsl:call-template name="menu" />
</xsl:template>
<xsl:template name="menu">
<xsl:for-each select="data/params">
<table>
<tr>
<td>
<xsl:value-of select="items_count" /> Items
</td>
<td>
<xsl:choose>
<xsl:when test="previous_page>0">
<xsl:element name="a" >
<xsl:attribute name="href" >#</xsl:attribute>
<xsl:attribute name="onclick">
loadGridPage(<xsl:value-of select="previous_page"/>)
</xsl:attribute> Previous page
</xsl:element>
</xsl:when>
</xsl:choose>
</td>
<td>
<xsl:choose>
<xsl:when test="next_page>0">
<xsl:element name="a">
<xsl:attribute name = "href" >#</xsl:attribute>
<xsl:attribute name = "onclick">
loadGridPage(<xsl:value-of select="next_page"/>)
</xsl:attribute> Next page
</xsl:element>
</xsl:when>
</xsl:choose>
</td>
<td>
page <xsl:value-of select="returned_page" />
of <xsl:value-of select="total_pages" />
</td>
</tr>
</table>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

9. Create grid.js:
// stores the reference to the XMLHttpRequest object var xmlHttp = createXmlHttpRequestObject();
// the name of the XSLT file
var xsltFileUrl = "grid.xsl";
// the file that returns the requested data in XML format var feedGridUrl = "grid.php";

// the id of the grid div var gridDivId = "gridDiv";
// the grid of the status div var statusDivId = "statusDiv";
// stores temporary row data var tempRow;
// the ID of the product being edited
var editableId = null;
// the XSLT document var stylesheetDoc;

// eveything starts here function init()
{
// test if user has browser that supports native XSLT functionality if(window.XMLHttpRequest && window.XSLTProcessor && window.DOMParser)
{
// load the grid loadStylesheet(); loadGridPage(1); return;
}
// test if user has Internet Explorer with proper XSLT support
if (window.ActiveXObject && createMsxml2DOMDocumentObject())
{
// load the grid
loadStylesheet();
loadGridPage(1);
// exit the function
return;
}
// if browser functionality testing failed, alert the user
alert("Your browser doesn't support the necessary functionality.");
}

function createMsxml2DOMDocumentObject()
{
// will store the reference to the MSXML object
var msxml2DOM;
// MSXML versions that can be used for our grid
var msxml2DOMDocumentVersions = new Array("Msxml2.DOMDocument.6.0",
"Msxml2.DOMDocument.5.0", "Msxml2.DOMDocument.4.0");
// try to find a good MSXML object
for (var i=0; i<msxml2DOMDocumentVersions.length && !msxml2DOM; i++)
{
try
{
// try to create an object
msxml2DOM = new ActiveXObject(msxml2DOMDocumentVersions[i]);
}
catch (e) {}
}
// return the created object or display an error message if (!msxml2DOM)
alert("Please upgrade your MSXML version from \n" +
"http://msdn.microsoft.com/XML/XMLDownloads/default.aspx");
else
return msxml2DOM;
}

// creates an XMLHttpRequest instance function createXmlHttpRequestObject()
{
// will store the reference to the XMLHttpRequest object
var xmlHttp;

// this should work for all browsers except IE6 and older try
{
// try to create XMLHttpRequest object xmlHttp = new XMLHttpRequest();
}
catch(e)
{
// assume IE6 or older
var XmlHttpVersions = new Array("MSXML2.XMLHTTP.6.0", "MSXML2.XMLHTTP.5.0",
"MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP");
// try every prog id until one works
for (var i=0; i<XmlHttpVersions.length && !xmlHttp; i++)
{
try
{
// try to create XMLHttpRequest object
xmlHttp = new ActiveXObject(XmlHttpVersions[i]);
}
catch (e) {}
}
}
// return the created object or display an error message if (!xmlHttp)
alert("Error creating the XMLHttpRequest object.");
else
return xmlHttp;
}

// loads the stylesheet from the server using a synchronous request function loadStylesheet()
{
// load the file from the server xmlHttp.open("GET", xsltFileUrl, false);
xmlHttp.send(null);
// try to load the XSLT document
if (this.DOMParser) // browsers with native functionality
{
var dp = new DOMParser();
stylesheetDoc = dp.parseFromString(xmlHttp.responseText, "text/xml");
}
else if (window.ActiveXObject) // Internet Explorer?
{
stylesheetDoc = createMsxml2DOMDocumentObject(); stylesheetDoc.async = false; stylesheetDoc.load(xmlHttp.responseXML);
}
}

// makes asynchronous request to load a new page of the grid function loadGridPage(pageNo)
{
// disable edit mode when loading new page editableId = false;
// continue only if the XMLHttpRequest object isn't busy
if (xmlHttp && (xmlHttp.readyState == 4 || xmlHttp.readyState == 0))
{
var query = feedGridUrl + "?action=FEED_GRID_PAGE&page=" + pageNo;
xmlHttp.open("GET", query, true);
xmlHttp.onreadystatechange = handleGridPageLoad;

// handle receiving the server response with a new page of products function handleGridPageLoad()
{
// when readyState is 4, we read the server response if (xmlHttp.readyState == 4)
{
// continue only if HTTP status is "OK" if (xmlHttp.status == 200)
{
// read the response
response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error") >= 0
|| response.length == 0)
{
// display error message
alert(response.length == 0 ? "Server serror." : response);
// exit function return;
}
// the server response in XML format xmlResponse = xmlHttp.responseXML;
// browser with native functionality?
if (window.XMLHttpRequest && window.XSLTProcessor &&
window.DOMParser)
{
// load the XSLT document
var xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(stylesheetDoc);
// generate the HTML code for the new page of products
page = xsltProcessor.transformToFragment(xmlResponse, document);
// display the page of products
var gridDiv = document.getElementById(gridDivId);
gridDiv.innerHTML = "";
gridDiv.appendChild(page);
}
// Internet Explorer code
else if (window.ActiveXObject)
{
// load the XSLT document
var theDocument = createMsxml2DOMDocumentObject(); theDocument.async = false; theDocument.load(xmlResponse);
// display the page of products
var gridDiv = document.getElementById(gridDivId);
gridDiv.innerHTML = theDocument.transformNode(stylesheetDoc);
}
}
else
{
alert("Error reading server response.")
}
}
}

// enters the product specified by id into edit mode if editMode is true,
// and cancels edit mode if editMode is false function editId(id, editMode)
{
// gets the <tr> element of the table that contains the table var productRow = document.getElementById(id).cells;
// are we enabling edit mode?
if(editMode)
{
// we can have only one row in edit mode at one time

if(editableId) editId(editableId, false);
// store current data, in case the user decides to cancel the changes
save(id);
// create editable text boxes productRow[1].innerHTML =
'<input type="text" name="name" ' +
'value="' + productRow[1].innerHTML+'">';
productRow[2].innerHTML =
'<input type="text" name="price" ' +
'value="' + productRow[2].innerHTML+'">';
productRow[3].getElementsByTagName("input")[0].disabled = false;
productRow[4].innerHTML = '<a href="#" ' +
'onclick="updateRow(document.forms.grid_form_id,' + id +
')">Update</a><br/><a href="#" onclick="editId(' + id +
',false)">Cancel</a>';
// save the id of the product being edited editableId = id;
}
// if disabling edit mode... else
{
productRow[1].innerHTML = document.forms.grid_form_id.name.value;
productRow[2].innerHTML = document.forms.grid_form_id.price.value;
productRow[3].getElementsByTagName("input")[0].disabled = true;
productRow[4].innerHTML = '<a href="#" onclick="editId(' + id +
',true)">Edit</a>';
// no product is being edited editableId = null;
}
}

// saves the original product data before editing row function save(id)
{
// retrieve the product row
var tr = document.getElementById(id).cells;
// save the data
tempRow = new Array(tr.length);
for(var i=0; i<tr.length; i++)
tempRow[i] = tr[i].innerHTML;
}

// cancels editing a row, restoring original values function undo(id)
{
// retrieve the product row
var tr = document.getElementById(id).cells;
// copy old values
for(var i=0; i<tempRow.length; i++)
tr[i].innerHTML = tempRow[i];
// no editable row editableId = null;
}

// update one row in the grid if the connection is clear function updateRow(grid, productId)
{
// continue only if the XMLHttpRequest object isn't busy
if (xmlHttp && (xmlHttp.readyState == 4 || xmlHttp.readyState == 0))
{
var query = feedGridUrl + "?action=UPDATE_ROW&id=" + productId + "&" + createUpdateUrl(grid);
xmlHttp.open("GET", query, true);
xmlHttp.onreadystatechange = handleUpdatingRow;

// handle receiving a response from the server when updating a product function handleUpdatingRow()
{
// when readyState is 4, we read the server response if(xmlHttp.readyState == 4)
{
// continue only if HTTP status is "OK" if(xmlHttp.status == 200)
{
// read the response
response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error") >= 0
|| response.length == 0)
alert(response.length == 0 ? "Server serror." : response);
// if everything went well, cancel edit mode
else
editId(editableId, false);
}
else
{
// undo any changes in case of error
undo(editableId);
alert("Error on server side.");
}
}
}

// creates query string parameters for updating a row function createUpdateUrl(grid)
{
// initialize query string
var str = "";
// build a query string with the values of the editable grid elements for(var i=0; i<grid.elements.length; i++)
switch(grid.elements[i].type)
{
case "text":
case "textarea":
str += grid.elements[i].name + "=" +
escape(grid.elements[i].value) + "&";
break;
case "checkbox":
if (!grid.elements[i].disabled)
str += grid.elements[i].name + "=" + (grid.elements[i].checked ? 1 : 0) + "&";
break;
}
// return the query string return str;
}

10. Finally, create grid.css:
body
{
font-family: Verdana, Arial;
font-size: 10pt
}

table
{
width: 500px;
}

td.right
{

color: darkblue; text-align: right; width: 125px
}

td.left
{
color: darkblue;
text-align: left;
width: 125px
}

table.list
{
border: black 1px solid;
}

th
{
text-align: left;
background-color: navy;
color: white
}

th.th1
{
width: 30px
}

th.th2
{
width: 300px
}

input.editName
{
border: black 1px solid;
width: 300px
}

input.editPrice
{
border: black 1px solid;
width: 50px
}

11. Load http://localhost/ajax/grid in your web browser, and test its functionality to make sure it works as expected (see Figures 8.1 and 8.2 for reference).

What Just Happened?
Let's dissect the code starting with the server-side functionality. At the heart of the server lies the database. In our case, we have a table called product with the following fields:

• product_id is the table's primary key, containing the numeric ID of the product.
• name is the product's name.
• price is the product's price.
• on_promotion is a bit field (should only take values of 0 or 1, although MySQL may permit more, depending on the version), which specifies if the product is on promotion. We used this field for our grid because it allows us to show how to use a checkbox to display the bit value.

As usual on the server, we have a PHP script, which in this case is named grid.php, that is the main access point for all asynchronous client requests.

grid.php expects to receive a query string parameter called action that tells it what action it is expected to perform. The possible values are:

• FEED_GRID_PAGE: This value is used to retrieve a page of products. Together with
this parameter, the server also expects a parameter named page, which specifies what page of products to return.
• UPDATE_ROW: This value is used to update the details of a row that was edited by the user. For this action, the server also expects to receive the new values for the product, in four parameters named id, name, price, and on_promotion.

To see the data generated by the server, make a simple call to http://localhost/ajax/grid/ grid.php?action=FEED_GRID_PAGE&page=1. Using the default database information, the output will look like Figure 8.3:

Figure 8.3: Server Returning the First Page of Products

On the client, this data will be parsed and transformed to the HTML grid using an XSL transformation. This code was tested with Mozilla and Internet Explorer, which at the time of writing supported the required functionality. Opera is expected to support XSL Transformations starting with version 9.

The XSL transformation code is defined in grid.xsl. Please see Appendix C at http://ajaxphp.packtpub.comfor a primer into the world of XSL, and refer one of the many available books and online resources for digging into the details. XSL is a really big subject, so be prepared for a lot of learning if you intend to master it.

The first function in the client script, grid.js, is init(). This function checks if the user's browser has the necessary features to perform the XSL transformation:

// eveything starts here function init()
{
// test if user has browser that supports native XSLT functionality
if(window.XMLHttpRequest && window.XSLTProcessor && window.DOMParser)
{
// load the grid
loadStylesheet(); loadGridPage(1); return;
}
// test if user has Internet Explorer with proper XSLT support if (window.ActiveXObject && createMsxml2DOMDocumentObject())
{
// load the grid loadStylesheet();
loadGridPage(1);
// exit the function return;
}
// if browser functionality testing failed, alert the user alert("Your browser doesn't support the necessary functionality.");
}

This function allows continuing if the browser is either Internet Explorer (in which case the user also needs a recent MSXML version), or a browser that natively supports the XMLHttpRequest, XSLTProcessor, and DOMParser classes.

The second function that is important to understand is loadStylesheet(). This function is called once when the page loads, to request the grid.xsl file from the server, which is loaded locally. The grid.xls file is loaded using a synchronous call, and then is stored using techniques specific to the user's browser, depending on whether the browser has native functionality, or it is Internet Explorer, in which case an ActiveXObject is used:

// loads the stylesheet from the server using a synchronous request function loadStylesheet()
{
// load the file from the server
xmlHttp.open("GET", xsltFileUrl, false);
xmlHttp.send(null);
// try to load the XSLT document
if (this.DOMParser) // browsers with native functionality
{
var dp = new DOMParser();
stylesheetDoc = dp.parseFromString(xmlHttp.responseText, "text/xml");
}
else if (window.ActiveXObject) // Internet Explorer?

{
stylesheetDoc = createMsxml2DOMDocumentObject();
stylesheetDoc.async = false;
stylesheetDoc.load(xmlHttp.responseXML);
}
}

The loadGridPage function is called once when the page loads, and then each time the user clicks Previous Page or Next Page, to load a new page of data. This function calls the server asynchronously, specifying the page of products that needs to be retrieved:

// makes asynchronous request to load a new page of the grid function loadGridPage(pageNo)
{
// disable edit mode when loading new page editableId = false;
// continue only if the XMLHttpRequest object isn't busy
if (xmlHttp && (xmlHttp.readyState == 4 || xmlHttp.readyState == 0))
{
var query = feedGridUrl + "?action=FEED_GRID_PAGE&page=" + pageNo;
xmlHttp.open("GET", query, true); xmlHttp.onreadystatechange = handleGridPageLoad; xmlHttp.send(null);
}
}

The handleGridPageLoad callback function is called to handle the server response. After the typical error handling mechanism, it reveals the code that effectively transforms the XML structure received from the server to HTML code that is displayed to the client. The transformation code is, again, browser-specific, performing functionality differently for Internet Explorer and for the browsers with native XLS support:

// the server response in XML format xmlResponse = xmlHttp.responseXML;
// browser with native functionality?
if (window.XMLHttpRequest && window.XSLTProcessor && window.DOMParser)
{
// load the XSLT document
var xsltProcessor = new XSLTProcessor();
xsltProcessor.importStylesheet(stylesheetDoc);
// generate the HTML code for the new page of products
page = xsltProcessor.transformToFragment(xmlResponse, document);
// display the page of products
var gridDiv = document.getElementById(gridDivId);
gridDiv.innerHTML = "";
gridDiv.appendChild(page);
}
// Internet Explorer code
else if (window.ActiveXObject)
{
// load the XSLT document
var theDocument = createMsxml2DOMDocumentObject();
theDocument.async = false;
theDocument.load(xmlResponse);
// display the page of products
var gridDiv = document.getElementById(gridDivId);
gridDiv.innerHTML = theDocument.transformNode(stylesheetDoc);
}

Then we have the editId function, which is called when the Edit or Cancel links are clicked in the grid, to enable or disable edit mode. When edit mode is enabled, the product name, its price, and
its promotion checkbox are transformed to editable controls. When disabling edit mode, the same elements are changed back to their non-editable state.

save() and undo() are helper functions used for editing rows. The save function saves the original product values, which are loaded back to the grid by undo if the user changes her or his mind about the change and clicks the Cancel link.

Row updating functionality is supported by the updateRow function, which is called when the Update link is clicked. updateRow() makes an asynchronous call to the server, specifying the new product values, which are composed into the query string using the createUpdateUrl helper function:

// update one row in the grid if the connection is clear function updateRow(grid, productId)
{
// continue only if the XMLHttpRequest object isn't busy
if (xmlHttp && (xmlHttp.readyState == 4 || xmlHttp.readyState == 0))
{
var query = feedGridUrl + "?action=UPDATE_ROW&id=" + productId + "&" + createUpdateUrl(grid);
xmlHttp.open("GET", query, true);
xmlHttp.onreadystatechange = handleUpdatingRow;
xmlHttp.send(null);
}
}

The handleUpdatingRow callback function has the responsibility to ensure that the product change is performed successfully, in which case it disables edit mode for the row, or displays an error message if an error happened on the server side:

// continue only if HTTP status is "OK" if(xmlHttp.status == 200)
{
// read the response
response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error") >= 0
|| response.length == 0)
alert(response.length == 0 ? "Server serror." : response);
// if everything went well, cancel edit mode
else
editId(editableId, false);
}

The technique for displaying the error was implemented in other exercises as well. If the server returned a specific error message, that message is displayed to the user. If PHP is configured not to output errors, the response from the server will be void, in which case we simply display a generic error message.

Summary
In this chapter you have implemented already familiar AJAX techniques to build a data grid. You have met XSL, which allows implementing very powerful architectures where the server side of your application doesn't need to deal with presentation.

Having XSL deal with formatting the data to be displayed to your visitors is the professional way
to deal with these kinds of tasks, and if you are serious about web development, it is recommended to learn XSL well. Beware; this will be time and energy consuming, but in the end the effort will
be well worth it.

0 comments: