Tuesday, July 14, 2009

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

AJAX Drag and Drop

When drag-and-drop capability was first introduced to websites, people looked at it with astonishment. This was really a great feature to provide via your website! Since then, JavaScript has evolved in people's eyes from a "check-out-that-snow-on-my-website" scripting language to a standardized and powerful "do-powerful-stuff-with-it" language.

Many frameworks and JavaScript toolkits have been developed, with new ones appearing frequently. script.aculo.us is one of the most popular JavaScript toolkits, and it allows implementing amazing effects in web pages—check out the examples on its official web page at http://script.aculo.us/. Script.aculo.us is an open-source JavaScript framework, distributed under an MIT-style license, so you can use it for anything you like, as long as you include the copyright notice. You can download script.aculo.us from http://script.aculo.us/downloads. Check out the documentation on http://wiki.script.aculo.us

In this chapter, you will learn how to integrate script.aculo.us features into your website, by building an AJAX database-enabled sortable list.

Using Drag and Drop on the Web
While exploring some existing web applications with drag-and-drop capability, we found out that there are at least two situations where drag and drop smoothes up the user interface and the interactivity between human and machine. Drag and drop can be successfully used in:

• Shopping carts
• Sortable lists

Shopping Carts
You're probably familiar with traditional e-commerce websites. In the light of the new AJAX boom, a new generation of shopping carts has appeared, where visitors have to use drag and drop to add products to their carts, instead of clicking an "Add to Cart" button. While one could argue
the real usefulness of this "feature" (my grandmother still prefers the button, she doesn't know how to drag and drop), the visual effect is pretty impressive.

A few websites have already put this into practice. One such example is Panic Goods—selling t-shirts! The URL for this is: http://www.panic.com/goods.

Notice the light blue bar on the bottom of the screen? That's the actual shopping cart. Just drag some t-shirts from the catalog, and drop them into the shopping cart, to see how the cart performs. Products are lined up in the cart and it's easy to see what you have chosen and for what amount. Drag items outside the light blue bar to remove them from the cart. Pretty impressive, isn't it?

Sortable Lists
There's a type of list we probably use daily, namely, a to-do list. We usually use yellow Post-its and some of us even use specialized software.

But with so many new web applications available out there, surely there must be a dozen to-do list applications! I'll just mention Ta-da Lists (http://www.tadalist.com), created by 37signals.
This company has actually reinvented the entire concept of web applications and has taken it to the next level. Ta-da Lists, one of its first products, is a tool that allows you to create several to-do
lists, each with its own items (things to do, that is). It's a really helpful tool and a lot of people use it, although most of them have upgraded to other 37signals products like Basecamp
(http://www.basecamphq.com) and Backpack (http://www.backpackit.com).

Despite its intuitive user interface and easy-to-use actions, Ta-da Lists lacks a very basic feature that would greatly increase its usability: dragging and dropping list items, thus reordering the list. To reorder a list in Ta-da Lists, you have to click on a link that will refresh the page and display four arrow buttons (bring to front, move up, move down, and send to back).

Although this implementation works well, a drag-and-drop system would make it faster and easier to use. 37signals have improved this functionality in Basecamp, though, and the to-do lists in there have draggable items—an upgrade that proves the usability of the drag-and-drop concept.

Building the AJAX Drag-and-Drop Sortable List
Application
One thing that sets this application apart from other applications we've built in this book is that in this case, we are going to use two external JavaScript frameworks: Prototype and script.aculo.us.

"Prototype is a JavaScript framework that aims to ease development of dynamic web applications." It was created by Sam Stephenson and is quickly becoming the JavaScript framework, because of its great functionality.

Prototype is distributed under an MIT-style license and it can be downloaded from
http://prototype.conio.net.

If you want to learn more about Prototype, check out the tutorial on
http://www.particletree.com/features/quick-guide-to-prototype.

The Prototype features are:

• Complete object-orientation
• Utility functions
• Form helper functions
• AJAX support
• Periodical executer

Another pioneer of JavaScript development is Thomas Fuchs, the man who built the great JavaScript library—script.aculo.us—a library that provides spectacular visual effects. We'll be using some of these features in our drag-and-drop application (more specifically, the dragging and dropping features). Script.aculo.us is built on top of Prototype, thus inheriting all Prototypes' features.

Features of Script.aculo.us are:

• Complete object-orientation
• Visual effects (fade in, fade out, grow, shrink, blind down, blind up, shake, etc.)
• Drag-and-drop support
• Autocompletion
• In-place editing
• Slider controls

The application we're about to build will be a small task management application and will allow us to create new tasks, reorder existing tasks, and delete tasks. Summarizing the features:

• Database back end
• Drag-and-drop items
• Add new tasks with AJAX
• Instant database update when drag and dropping
• Delete a task by dragging and dropping it into a special designated area

Let's take a look at how this is going to look:

Figure 10.1: Add, Reorder, and Delete Tasks, in a Simple Visual Interface

Dragging items around the screen makes the other items switch positions.

When dropping a task on the DROP HERE TO DELETE area, a confirmation is required before the application proceeds with the actual deletion; as shown in the following figure:

Figure 10.2: Confirmation Required Before Deleting a Task

Time for Action—Task Management Application with AJAX
1. Connect to the ajax database, and create a table named tasks with the following code:
CREATE TABLE tasks (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
order_no INT UNSIGNED NOT NULL default '0', description VARCHAR(100) NOT NULL default '', PRIMARY KEY (id)
);

2. In your ajax folder, create a new folder named drag-and-drop.
3. In the drag-and-drop folder, create a file named config.php, and add the database configuration code to it:
<?php
// defines database connection data
define('DB_HOST', 'localhost'); define('DB_USER', 'ajaxuser'); define('DB_PASSWORD', 'practical'); define('DB_DATABASE', 'ajax');
?>

4. Now add the standard error-handling file, error_handler.php. Feel free to copy this file from previous chapters. Anyway, here's the code for it:
<?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
if(ob_get_length()) 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;
}
?>

5. Download the script.aculo.us library from http://script.aculo.us/downloads and unzip/untar the downloaded archive to your drag-and-drop folder. Change the script.aculo.us folder name from something like scriptaculous-js-x.y.z to simply scriptaculous.
6. Create a new file named index.php, and add this code to it:
<?php
require_once ('taskslist.class.php');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>AJAX Drag and Drop Sortable List</title>
<link href="drag-and-drop.css" rel="stylesheet" type="text/css" />
<script src="drag-and-drop.js" type="text/javascript"></script>
<script src="scriptaculous/lib/prototype.js" type="text/javascript">
</script>
<script src="scriptaculous/src/scriptaculous.js" type="text/javascript">
</script>
</head>
<body onload="startup()">
<h1>Task Management</h1>
<h2>Add a new task</h2>
<div>
<input type="text" id="txtNewTask" name="txtNewTask"
size="30" maxlength="100" onkeydown="handleKey(event)"/>
<input type="button" name="submit" value="Add this task" onclick="process('txtNewTask', 'addNewTask')" />
</div>
<br />
<h2>All tasks</h2>
<ul id="tasksList" onmouseup="process('tasksList', 'updateList')">
<?php
$myTasksList = new TasksList();
echo $myTasksList->BuildTasksList();
?>

</ul>
<br /><br />
<div id="trash">
DROP HERE TO DELETE
<br /><br />
</div>
</body>
</html>

7. Create a new file named taskslist.class.php, and add this code to it:
<?php
// load error handler and database configuration
require_once ('error_handler.php');
require_once ('config.php');

// This class builds a tasks list and
// performs add/delete/reorder actions on it class TasksList
{
// stored database connection private $mMysqli;

// constructor opens database connection function __construct()
{
// connect to the database
$this->mMysqli = new mysqli(DB_HOST, DB_USER, DB_PASSWORD,
DB_DATABASE);
}

// destructor closes database connection public function __destruct()
{
$this->mMysqli->close();
}

// Builds the tasks list
public function BuildTasksList()
{
// initialize output
$myList = '';
// build query
$result = $this->mMysqli->query('SELECT * FROM tasks ' .
'ORDER BY order_no ASC');
// build task list as <li> elements
while ($row = $result->fetch_assoc())
{
$myList .= '<li id="' . htmlentities($row['id']) . '">' .
htmlentities($row['description']) . '</li>';
}
// return the list
return $myList;
}

// Handles the server-side data processing public function Process($content, $action)
{
// perform action requested by client switch($action)
{
// Reorder task list case 'updateList':
// retrieve update details
$new_order = explode('_', $content);
// update list

for ($i=0; $i < count($new_order); $i++)
{
// escape data received from client
$new_order[$i] =
$this->mMysqli->real_escape_string($new_order[$i]);
// update task
$result = $this->mMysqli->query('UPDATE tasks SET order_no="' .
$i . '" WHERE id="' . $new_order[$i] . '"');
}
$updatedList = $this->BuildTasksList();
return $updatedList;
break;

// Add a new task case 'addNewTask':
// escape input data
$task = trim($this->mMysqli->real_escape_string($content));
// continue only if task name is not null if ($task)
{
// obtain the highest order_no
$result = $this->mMysqli->query('SELECT (MAX(order_no) + 1) ' .
'AS order_no FROM tasks');
$row = $result->fetch_assoc();
// if the table is empty, order_no will be null
$order = $row['order_no'];
if (!$order) $order = 1;
// insert the new task as the bottom of the list
$result = $this->mMysqli->query
('INSERT INTO tasks (order_no, description) ' .
'VALUES ("' . $order . '", "' . $task . '")');
// return the updated tasks list
$updatedList = $this->BuildTasksList();
return $updatedList;
}
break;

// Delete task case 'delTask':
// escape input data
$content = trim($this->mMysqli->real_escape_string($content));
// delete the task
$result = $this->mMysqli->query('DELETE FROM tasks WHERE id="' .
$content . '"');
$updatedList = $this->BuildTasksList();
return $updatedList;
break;
}
}
}
?>

8. Create a new file named drag-and-drop.php, and add this code to it:
<?php
// load helper class
require_once ('taskslist.class.php');
// create TasksList object
$myTasksList = new TasksList();
// read parameters
$action = $_GET['action'];
$content = $_GET['content'];
// 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/html');
// execute the client request and return the updated tasks list echo $myTasksList->Process($content, $action);
?>

9. Create a new file named drag-and-drop.js, and add this code to it:
// holds an instance of XMLHttpRequest
var xmlHttp = createXmlHttpRequestObject();
// when set to true, display detailed error messages var showErrors = true;
// initialize the requests cache
var cache = new Array();

// 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) {} // ignore potential error
}
}
// return the created object or display an error message if (!xmlHttp)
alert("Error creating the XMLHttpRequest object.");
else
return xmlHttp;
}

// function that displays an error message function displayError($message)
{
// ignore errors if showErrors is false if (showErrors)
{
// turn error displaying Off showErrors = false;
// display error message
alert("Error encountered: \n" + $message);

}
}

// Scriptaculous-specific code to define a sortable list and a drop zone function startup()
{
// Transform an unordered list into a sortable list with draggable items
Sortable.create("tasksList", {tag:"li"});

// Define a drop zone used for deleting tasks
Droppables.add("trash",
{
onDrop: function(element)
{
var deleteTask =
confirm("Are you sure you want to delete this task?")
if (deleteTask)
{

}
}
});
}

Element.hide(element);
process(element.id, "delTask");

// Serialize the id values of list items (<li>s)
function serialize(listID)
{
// count the list's items
var length = document.getElementById(listID).childNodes.length;
var serialized = "";
// loop through each element
for (i = 0; i < length; i++)
{
// get current element
var li = document.getElementById(listID).childNodes[i];
// get current element's id without the text part var id = li.getAttribute("id");
// append only the number to the ids array serialized += encodeURIComponent(id) + "_";
}
// return the array with the trailing '_' cut off return serialized.substring(0, serialized.length - 1);
}

// Send request to server
function process(content, action)
{
// only continue if xmlHttp isn't void if (xmlHttp)
{
// initialize the request query string to empty string params = "";
// escape the values to be safely sent to the server content = encodeURIComponent(content);
// send different parameters depending on action
if (action == "updateList")
params = "?content=" + serialize(content) + "&action=updateList";
else if (action == "addNewTask")
{
// prepare the task for sending to the server var newTask =
trim(encodeURIComponent(document.getElementById(content).value));
// don't add void tasks if (newTask)
params = "?content=" + newTask + "&action=addNewTask";

}
else if (action =="delTask")
params = "?content=" + content + "&action=delTask";
// don't add null params to cache if (params) cache.push(params);

// try to connect to the server try
{
// only continue if the connection is clear and cache is not empty if ((xmlHttp.readyState == 4 || xmlHttp.readyState == 0)
&& cache.length>0)
{
// get next set of values from cache
var cacheEntry = cache.shift();
// initiate the request
xmlHttp.open("GET", "drag-and-drop.php" + cacheEntry, true);
xmlHttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xmlHttp.onreadystatechange = handleRequestStateChange;
xmlHttp.send(null);
}
else
{
setTimeout("process();", 1000);
}
}
// display the error in case of failure catch (e)
{
displayError(e.toString());
}
}
}

// function that retrieves the HTTP response function handleRequestStateChange()
{
// when readyState is 4, we also read the server response if (xmlHttp.readyState == 4)
{
// continue only if HTTP status is "OK" if (xmlHttp.status == 200)
{
try
{
postUpdateProcess();
}
catch(e)
{
// display error message displayError(e.toString());
}
}
else
{
displayError(xmlHttp.statusText);
}
}
}
// Processes server's response function postUpdateProcess()
{
// read the response
var response = xmlHttp.responseText;
// server error?

if (response.indexOf("ERRNO") >= 0 || response.indexOf("error") >= 0)
alert(response);
// update the tasks list document.getElementById("tasksList").innerHTML = response; Sortable.create("tasksList"); document.getElementById("txtNewTask").value = ""; document.getElementById("txtNewTask").focus();
}
/* handles keydown to detect when enter is pressed */
function handleKey(e)
{
// get the event
e = (!e) ? window.event : e;
// get the code of the character that has been pressed
code = (e.charCode) ? e.charCode : ((e.keyCode) ? e.keyCode : ((e.which) ? e.which : 0));
// handle the keydown event if (e.type == "keydown")
{
// if enter (code 13) is pressed if(code == 13)
{
// send the current message process("txtNewTask", "addNewTask");
}
}
}

/* removes leading and trailing spaces from the string */
function trim(s)
{
return s.replace(/(^\s+)|(\s+$)/g, "")
}

10. Create a new file named drag-and-drop.css, and add this code to it:
body
{
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}
ul.sortableList
{
list-style-type: none;
padding: 0px;
margin: 0px;
width: 300px;
}
ul.sortableList li
{
cursor: move;
padding: 2px 2px;
margin: 2px 0px;
border: 1px solid #00CC00;
background-color: #F4FFF5;
}
h1
{
border-bottom: 1px solid #cccccc;
}
#trash
{
border: 4px solid #ff0000;
width: 270px;
padding: 10px;
}

11. Load http://localhost/ajax/drag-and-drop in your web browser and test its functionality to make sure it works as expected (see Figures 10.1 and 10.2 for reference).

What Just Happened?
Adding a task is performed as mentioned in the following steps:
1. The user enters task.
2. When the user clicks on Add this task button or presses Enter, the data is sent to the server with an asynchronous HTTP request. The server script inserts the new task into the database, and returns the updated list, which is then injected into the code with JavaScript.

When reordering the list, this is what happens:
1. Every task is an XHTML list element: an <li>. The user begins dragging an item; on dropping it, an HTTP request is sent to the server. This request consists of a
serialized string of IDs, every list element's ID.
2. On the client you'll see the list reordered, while the server updates the order of each element in the database.

This is how deleting a task works:

1. The user drags an item and drops it on the DROP HERE TO DELETE area.
2. An HTTP request is sent to the server, which performs the task deletion from the database and the XHTML element is instantly destroyed.

We include in index.php the JavaScript libraries we'll be using:

<script src="drag-and-drop.js" type="text/javascript"></script>
<script src="scriptaculous/lib/prototype.js" type="text/javascript">
</script>
<script src="scriptaculous/src/scriptaculous.js" type="text/javascript">
</script>

The first line includes our custom functions and AJAX-related tasks. The second line includes the
Prototype library, while the third line includes the script.aculo.us library.

The onload event inside the <body> tag calls the startup() function, which defines the unordered list with id="tasksList" as a sortable element (Sortable.create). This ensures drag-and-drop functionality for <li> elements inside the list. The startup() function also defines a droppable element Droppables.add; we use this as an area where we delete tasks.

Also, inside the startup() function, we define a behavior for dropping a list item on the drop zone:

onDrop: function(element)
{
var deleteTask = confirm("Are you sure you want to delete this task?")
if (deleteTask == true)
{
Element.hide(element);
process(element, "delTask");
}
}

This code asks the user for confirmation, and if this is received hides that element from the screen and calls process, which sends the HTTP request.

In index.php, there's a small block of code that dynamically creates the tasks list:

<ul id="tasksList" onmouseup="process('tasksList', 'updateList')">
<?php
$myTasksList = new TasksList();
echo $myTasksList->BuildTasksList();
?>
</ul>

A new task is added by clicking on the Add this task button or by pressing the Enter key.

The actual AJAX request is sent by the process function. This function handles the sending of requests for all three actions (reorder list / add task / delete task), by specifying the action to be performed as a parameter.

When adding a new task, the first parameter of the process function is the ID of the text field in which we've just typed a new task.

<input type="button" name="submit" value="Add this task" onclick="process('txtNewTask', 'addNewTask')" />

The database update after list reordering is triggered by an onmouseup event inside the unordered list with id="tasksList"—our sortable list. The event calls the process function, which takes as its first parameter the list's ID.

<ul id="tasksList" onmouseup="process('tasksList',
'updateList')">

Because we'll be sending an array of values to the server, we need to serialize that data and we do this through serialize, our home-made function. This function counts how many <li> elements we've got, then loops through each one of them and add its ID to the string. We also need to cut off the trailing '_' on the returned value.

function serialize(listID)
{
// count the list's items
var length = document.getElementById(listID).childNodes.length;
var serialized = "";
// loop through each element for (i = 0; i < length; i++)
{
// get current element
var li = document.getElementById(listID).childNodes[i];
// get current element's id without the text part
var id = li.getAttribute("id");
// append only the number to the ids array serialized += encodeURIComponent(id) + "_";
}
// return the array with the trailing '_' cut off return serialized.substring(0, serialized.length - 1);
}

Remember that XMLHttpRequest cannot make two HTTP requests at the same time, so if the object is busy processing a previous request, we save the details of the current request for later. This is particularly useful when the connection to the network or the Internet is slow. The request

details are saved using a cache system with the properties of a FIFO structure. Luckily, the JavaScript's Array class offers the exact functionality we need (through its push and shift methods), and hence we use it for caching purposes:

var cache = new Array();

So, in process(), before sending a new request to the server, we save the current request to the cache.
// only continue if xmlHttp isn't void if (xmlHttp)
{
if (action)
cache.push(content + "&" + action);

This adds a new element at the end of our cache array, an element that is created of two parts, a content (the ID of an HTML element) and an action to be performed by the server, separated by
'&'. Note that the new element is added only if action is not null, which happens when the
function is called not upon user's request, but to check if there are any pending actions to be made.

Afterwards, if the XMLHttpRequest object is free to start making other calls, we use shift() to get the last action from the cache and perform it. Note that, however, this value may not be the one
just added using push—in FIFO scenarios, the oldest record is processed first.

// try to connect to the server try
{
// continue only if the XMLHttpRequest object isn't busy
// and the cache is not empty
if ((xmlHttp.readyState == 4 || xmlHttp.readyState == 0)
&& cache.length>0)
{
// get next set of values from cache
var cacheEntry = cache.shift();

If the HTTP status is 0 or 4 it means that there are no active requests and we can send a new request. To send a new request we first read the data back from the cache, and split the current entry into two variables:

// split the array element
var values = cacheEntry.split("&");
content = values[0];
action = values[1];

Depending on these variables, we'll be sending different values as parameters:

// send different parameters depending on action if (action == "updateList")
params = "content=" + serialize(content) + "&action=updateList";
else if (action == "addNewTask")

params = "content=" + document.getElementById(content).value + "&action=addNewTask";
else if (action =="delTask")
params = "content=" + content + "&action=delTask";

These pieces of data are then used to make the server request:

xmlHttp.open("POST", "drag-and-drop.php", true); xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); xmlHttp.onreadystatechange = handleRequestStateChange;
xmlHttp.send(params);

The server's response is handled by the handleRequestStateChange function, which in turn calls postUpdateProcess(). Here we retrieve the server's response, which will either be an error message or a string containing HTML code for the updated list:

// read the response
var response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0 || response.indexOf("error") >= 0)
alert(response);
// update the tasks list
document.getElementById("tasksList").innerHTML = response; Sortable.create("tasksList"); document.getElementById("txtNewTask").value = ""; document.getElementById("txtNewTask").focus();
The last two lines of code clear the "new task" text field and set the cursor focus on that field. The drag-and-drop.php script is really light. We include taskslist.class.php, initiate a new
TasksList object and return the updated list after we call the class method Process, which will
perform one of the three possible actions: add task, reorder list, or delete task.

The taskslist.class.php file is the class we're using to perform server-side actions on our tasks list. Its constructor creates a database connection. Then, we have other two public methods:

• BuildTasksList creates list items with each task;
• Process takes two parameters, $content and $action. The first parameter holds user data and depends on the other parameter, which tells the script what actions should be performed.

When updating the list (case 'updateList'), we extract the values from the $content array, which holds a serialized string of the new order of <li> elements—the tasks, that is. Next we loop through extracted values and update the database.

To add a new task, we first escape user input with the mysqli method real_escape_string. Next, we need to get from the database the greatest order number that exists and increment it. This will be our new task's order number. We then insert the task in the database and return a string containing the order number and the task's description. This is sent back to the client, which will build a new list element, based on the received data.

When deleting a task (case 'delTask') is required, the only thing we do is delete the task from the database.

Every method returns a string with the new task list, namely a string of <li> elements.

Always filter user data

If you want to save yourself from a lot of trouble you should always filter user input. We used JavaScript's encodeURIComponent function when sending data to the server. On the server, we used the real_escape_string method of the mysqli object, to prevent SQL injection. Also on the server, we used the htmlentities PHP function to prepare the text that we send back to the client.

Summary
This is it! You've now got a working task management application with drag-and-drop support—all this with writing only a small amount of code. The next step in developing this application would be to make each task editable by double-clicking on it. script.aculo.us provides a great way of doing this with Ajax.InPlaceEditor. Check out the documentation on http://wiki.script.aculo.us/ scriptaculous/show/Ajax.InPlaceEditor for more information on how to accomplish this.

Another practical application for sortable lists would be in a Content Management System (CMS)—to manage the order of pages, projects, products, news, etc. In the end, it all depends on your imagination and how far you are willing to go to create great user interfaces.

A
Preparing Your Working
Environment

In order to avoid any headaches while going through the case studies in this book, it's best to install the necessary software and configure your environment the right way from the start. Although we assume you already have some experience developing PHP applications, we'll quickly go through the steps to install your machine with the necessary software.

The good news is that all the required software is free, powerful, and (finally!) comes with installers that make the programs easy for anyone to set up and configure. The bad news is that there are many possible configurations, so the instructions written might not apply 100% to you (for example, if you are using Windows, you may prefer to use IIS instead of Apache, and so on).

We'll cover the installation instructions separately for Windows and *nix based machines. We'll also cover preparing the database that is used in many examples throughout the book; these instructions apply to both Windows and *nix users, so be sure not to miss this section at the end of the appendix.

To build websites with AJAX and PHP you will need (quite unsurprisingly) to install PHP. The preferred version is PHP 5, because we use some of its features in Chapter 11. You also need a web server. We will cover installing Apache, which is the web server preferred by most PHP developers and web hosting companies. Because we've tried to make the examples in this book as relevant as possible for real-world scenarios, many of them need a database. We cover installing MySQL, which is the most popular database server in the PHP world. Because we used simple SQL code, you can easily use another database server without major code changes, or older versions of MySQL.

At the end of this chapter, we'll cover installing phpMyAdmin, which is a very useful web tool for administering your databases. You'll then learn how to use this tool to create a new database, and then a database user with full privileges to this database.

In the following pages, you'll learn how to:

• Install Apache 2, PHP 5, and MySQL 5 on your Windows machine
• Install Apache 2, PHP 5, and MySQL 5 on your *nix machine
• Install phpMyAdmin
• Create a new database and then a database user using phpMyAdmin

TIP

Programmers who don't want to install the required software manually have the option of using a software package such as XAMPP, which bundles all of them (and many more) in a single installer file. XAMPP is packaged for Linux, Windows, Mac OS X, and Solaris, and is free of charge. You can get XAMPP from http://www.apachefriends.org/ en/xampp.html.

If you decide to use XAMPP, you can skip directly to setting up the ajax database, as shown at the end of this appendix.

Preparing Your Windows Playground
Here we cover installing these software products in your Windows machine:

• Apache 2
• PHP 5
• MySQL 5

Installing Apache
You can download the latest MSI Installer version of the Apache HTTP Server from http://httpd.apache.org/download.cgi. Download the latest Win32 Binary (MSI Installer), which should have a name like apache_2.x.y-win32-x86-no_ssl.msi, then execute it.

The default installation location of Apache 2 is C:\Program Files\Apache Group\Apache2\, but the installer allows you to specify a different path. You can choose a more convenient location (such as C:\Apache), which can make your life working with Apache easier.

During installation you'll be asked to enter your server's information:

Figure A.1: Installing Apache 2.0

If you're not sure about how to complete the form, just type localhost for the first two fields, and write your email address for the last. You can change this information later by editing the Apache configuration file. The default location of this file is C:\Program Files\Apache Group\Apache2\ conf\httpd.conf.

You can also choose to install Apache on Port 80, or on Port 8080. The default port is 80, but if you already have a web server (such as IIS) on your machine, you'll need to install Apache on a different port. If you choose to run Apache on Port 8080, you will need to start the Apache service manually by going to the Apache bin folder (by default C:\Program Files\Apache Group\ Apache2\bin), and typing

apache -k install

When the web server runs on a port different than 80, you need to specify the port manually when making HTTP requests, such as in http://localhost:8080/ajax/suggest.

In the next setup screens, you can safely use the default options.

Along with the Apache server the installer will also start the Apache Service Monitor program, which is available from the taskbar. The taskbar icon reflects the current state of the web server (stopped, running, etc.), and also allows you to start, stop, or restart the Apache service.

Keep in mind that you'll need to restart (or stop and then start) the Apache service after making any changes to the httpd.conf configuration file, in order for the changes to become effective.

After installing Apache 2, make sure it works OK. If you installed it on port 80, browse to http://localhost/. If you installed it on 8080, go to http://localhost:8080/. You should see an Apache welcome message similar to this:

Figure A.2: Apache Installed Successfully

Installing MySQL
The official website of MySQL is http://www.mysql.com. At the time of this writing the latest stable version is MySQL 5.0, and you can download it from http://dev.mysql.com/downloads/ mysql/5.0.html. However, it's good to know that we made our SQL queries compliant with the SQL 92 standard, so you should be able to reuse them with other database systems with minimum of translation effort.

In the Downloads page, scroll down to the Windows downloads section, and download the Windows Essentials file. You'll be asked to choose a mirror site, and the file will be named something like mysql-essential-5.0.xx-win32.msi. After downloading the file, simply execute it to install your MySQL Server.

After installation you'll be given the chance to configure your server. Do so. It's safe to use the default options all the way through. At some point you'll need to set the root password, which will correspond to the root@localhost user. Choose a password that's complicated enough for others not to guess and simple enough for you to remember.

Before going through any case studies in this book, remember to see the Preparing the AJAX Database section at the end of this appendix.

Installing PHP
The official website of PHP is http://www.php.net. Start by downloading from the Windows Binaries section the latest PHP 5 zip package (not the installer!) from http://www.php.net/ downloads.php. We prefer not to use the installer because it doesn't include extensions that you may need for your projects, and it doesn't do much configuration work for you anyway.

After you download the Windows binaries, follow these steps to install PHP:

1. Unzip the zip package (it should be a file with a name like php-5.x.y-win32.zip) into a folder named C:\PHP\. You can choose another name or location for this folder if you want.
2. Copy php.ini-recommended from C:\PHP\ to your Windows folder (C:\Windows), renaming it as php.ini.
3. Open php.ini for editing with the text editor of your choice (such as Notepad) and uncomment the php_gd2.dll, php_mysql.dll, and php_xsl.dll extension lines (by removing the leading semicolons), and add a similar line for php_mysqli:
extension=php_gd2.dll extension=php_mysql.dll extension=php_mysqli.dll extension=php_xsl.dll

4. We recommend enabling full error reporting for PHP on the development machine, but this is optional (this option is the default). Be warned that this change can alter the functionality of other scripts on your server. Find the error_reporting line in php.ini and change it to:
error_reporting = E_ALL

5. Copy php5ts.dll and libmysql.dll located in C:\PHP\, to the Windows System32
folder (by default \Windows\System32).
6. Copy php_mysql.dll, php_mysqli.dll, php_xsl.dll, and php_gd2.dll from
C:\PHP\ext, to the Windows System32 folder.
7. Open the Apache configuration file for editing. By default, the location of this file is
C:\Program Files\Apache Group\Apache2\conf\httpd.conf.
8. In httpd.conf, find the portion with many LoadModule entries, and add the following lines:
LoadModule php5_module c:/php/php5apache2.dll
AddType application/x-httpd-php .php

9. Also in httpd.conf, find the DirectoryIndex entry, and add index.php at the end of the line, like this:
DirectoryIndex index.html index.html.var index.php

10. Save the httpd.conf file, and then restart the Apache 2 service, using the Apache Service Monitor by clicking its icon in the Notification Area of the taskbar. (If you get any error at this point, make sure that you followed correctly all the previous steps of the exercise.) If Apache restarts without generating any errors, that is a good sign.

11. Create a folder called ajax under the htdocs folder (by default C:\Program Files\ Apache Group\Apache2\htdocs).
12. To make sure that your Apache instance can also correctly parse PHP code, create a file named test.php in the ajax folder, and then add the following code to it:
<?php phpinfo();
?>

13. Point your web browser to http://localhost/ajax/test.php (or
http://localhost:8080/ajax/test.php if you installed Apache to work on port
8080) to test if everything went OK with the installation. You should get a page like this:

Figure A.3: PHP Installation Working

Congratulations, you just finished installing Apache, MySQL, and PHP!

The configuration set up isn't yet finished. If you're running Windows (and you probably are, since you're reading this), please skip the Preparing Your *nix Playground section, and go through the Installing phpMyAdmin and Preparing the AJAX Database sections at the end of this appendix.

Preparing Your *nix Playground
Almost all the UNIX and Linux distributions include Apache, PHP, and MySQL; however, you should check the versions of these programs. It would be good to have MySQL 4.1 or newer, and it's very important to have at least PHP 5. The code in this book will not work with older versions of PHP.

Installing Apache
To install Apache on your Unix-based server, follow these simple steps:

1. First, download the latest Apache Unix Source code for your system from http://httpd.apache.org/download.cgi and decompress it with a command such as:
tar -zxvf httpd-2.0.55.tar.gz

2. To compile and install the Apache Web Server on your system, go to the folder containing the sources and execute the following commands, while logged in as root:

./configure --prefix=/usr/local/apache2 --enable-so --enable-ssl --with- ssl --enable-auth-digest

make

make install

Installing MySQL
The official website of MySQL is http://www.mysql.com. At the time of this writing the latest stable version is MySQL 5.0, and you can download it from http://dev.mysql.com/downloads/ mysql/5.0.html. However, it's good to know that we made our SQL queries compliant with the SQL 92 standard, so you should be able to reuse them with other database systems with minimum of translation effort. Chapter 2 of the MySQL 5 manual covers installation procedures for all supported platforms, and you can read it here: http://dev.mysql.com/doc/refman/5.0/ en/installing.html.

If your Linux distribution supports RPMs, you'll need to download the RPMs for Server, Client programs, and Libraries and header files. Install MySQL as explained in the manual at http://dev.mysql.com/doc/refman/5.0/en/linux-rpm.html. If your platform doesn't support RPMs, install MySQL as explained at http://dev.mysql.com/doc/refman/5.0/en/installing- binary.html.

After installing MySQL, you should change the MySQL administrator's password (the root@localhost user), which is blank by default. Read more about MySQL passwords at http://dev.mysql.com/doc/mysql/en/Passwords.html. One way to change root's password is to execute:

mysqladmin -u root password 'your_new_password.'

Alternatively, you can access MySQL through a console program or by using a database administration tool such as phpMyAdmin, and execute this command:

SET PASSWORD FOR root@localhost=PASSWORD('your_new_password');

You can now test your MySQL server by executing the following command in your console:

#mysql -u root -p

Installing PHP
Every time you want to get a new PHP library working on Linux, you need to recompile the PHP module. That's why it's recommended to make a good compilation, with all the needed libraries, from the start.

1. Go to http://www.php.net/downloads.php and get the complete source code archive of PHP 5.x and extract the contents into a directory. At the time of writing, the latest PHP version was 5.1.2.
2. Go to the folder where you extracted the PHP source and execute the following commands:
./configure --with-config-file-path=/etc --with-mysql=/usr/include/mysql
--with-apxs2=/usr/local/apache2/bin/apxs --with-zlib --with-gd --with-xsl

make

make install

If you are compiling PHP for XAMPP, you need to use the following configure
command instead:

./configure --with-config-file-path=/opt/lampp/etc --with-mysql=/opt/lampp
--with-apxs2=/opt/lampp/bin/apxs --with-zlib --with-gd

After executing make and make install, you need to copy the newly created
php_src/libs/libphp5.so file to /opt/lampp/modules/libphp5.so.

3. Copy php.ini-recommended to /etc/php.ini by executing the following command:
cp php.ini-recommended /etc/php.ini.

4. Open the Apache configuration file (httpd.conf), find the DirectoryIndex entry, and make sure you have index.php at the end of the line:
DirectoryIndex index.html index.html.var index.php

5. Restart your Apache Web Server using the following command:
/usr/local/apache2/bin/apachectl restart

6. Create a folder called ajax under the htdocs folder (by default /usr/local/
apache2/htdocs/).
7. To make sure your PHP installation works, create a file named test.php in the ajax
folder you've just created, with the following contents in it:
<?php phpinfo();
?>

8. Finally, point your web browser to http://localhost/test.php, to ensure PHP
was correctly installed under Apache (you should get a page similar to Figure A.3).

Installing phpMyAdmin
phpMyAdmin is a very popular MySQL administration tool written in PHP. It allows you to manage your MySQL databases using a simple-to-use web interface. The official web page is http://www.phpmyadmin.net. Follow these steps to install and configure this program:

1. Start by downloading the latest version of phpMyAdmin from http://www.phpmyadmin.net/home_page/downloads.php. If you aren't sure what file to download, the safest bet is to go with the zip archive.
2. Unzip the archive somewhere on your disk. The archive contains a folder named
with the complete phpMyAdmin version (for example, at the time of this writing, the folder for the beta version of phpMyAdmin is called phpMyAdmin-2.8.0-beta1).
3. To make your life easier, rename this folder to simply phpMyAdmin.
4. Move the phpMyAdmin folder to the htdocs folder of Apache 2 (by default
C:\Program Files\Apache Group\Apache2\htdocs).
5. To make sure your phpMyAdmin installation is accessible by Apache, load http://localhost/phpMyAdmin in your favorite web browser. If everything worked OK, you should get a message such as this:

Figure A.4: phpMyAdmin Doesn’t Have Access to MySQL

6. The error message is suggestive enough—you need to instruct phpMyAdmin how to access your MySQL server. Under the phpMyAdmin folder search for a file named config.inc.php. If you find this file, change its options as shown in the following code snippet. If you don't find this file, create it with the following contents:

<?php
$cfg['PmaAbsoluteUri'] = "http://localhost/phpMyAdmin/";
$cfg['Servers'][1]['host'] = "localhost";
$cfg['Servers'][1]['auth_type'] = 'config';
$cfg['Servers'][1]['user'] = "root";
$cfg['Servers'][1]['password'] = "password";
?>

For more details on installing and using phpMyAdmin, see its documentation at http://www.phpmyadmin.net/home_page/docs.php. Packt Publishing has a separate book for those of you who want to learn more about phpMyAdmin—Mastering phpMyAdmin for Effective MySQL Management (ISBN: 1-904811-03-5). In case you're not a native English speaker, it's good to know that the book is also available in Czech, German, French, and Italian.

Preparing the AJAX Database
As an exercise for both using phpMyAdmin and working with MySQL, let's create a database called ajax, and create a MySQL user with full privileges to this database. You'll use this database and this user for all the exercises in this book. Follow these steps:

1. Load http://localhost/phpMyAdmin in your web browser. If the configuration data you wrote in config.inc.php was correct, you should see something like this:

Figure A.5: phpMyAdmin in Action

2. Write ajax in the Create a new database box, and then click the Create button.
3. phpMyAdmin doesn't have the visual tools to create new users, so you'll need to write some SQL code now. You need to create a user with full access to the ajax database, which will be used in all the case studies throughout the book. This user will be called (surprise!) ajaxuser, and its password will be practical. To add this user, click the SQL tab at the top of the page, and write this code in it:
GRANT ALL PRIVILEGES ON ajax.*
TO ajaxuser@localhost IDENTIFIED BY "practical"

SQL does sound a bit like plain English, but a few things need to be mentioned. The * in ajax.* means all objects in the ajax database. So this command tells MySQL "give all possible privileges to the ajax database to a user of this local machine called ajaxuser, whose password is practical".

4. Click the Go button.

Congratulations, you're all set for your journey through this book. Have fun learning AJAX!

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

AJAX RSS Reader

In the last few years, the Web has become much more active than it used to be. Today, we see an explosion of new sources of information, such as news sites appearing every day (such as http://www.digg.com and http://www.newsvine.com), and the growing trend of web life— weblogs (every person seems to have a weblog these days).

As a natural reaction to this invasion of information, many systems that allow grouping, filtering, and aggregating this information have appeared. This is implemented in practice through web syndication, which is that form of syndication where parts of a website (such as news, weblog posts, articles, and so on) are made available for other sites or applications to use.

In order to be usable by other parties, the data to be shared must be in a generic format that can be laid out in different formats than in the original source, and when it comes to such formats, RSS
2.0 and Atom are the most popular choices.

Learn more about the history of RSS and Atom in the Wikipedia—the link to the RSS page is
http://en.wikipedia.org/wiki/RSS_(protocol).

In this chapter, we'll analyze the RSS file format, then take a look at Google Reader (Google's
RSS aggregator), and then build our own RSS aggregator web page with AJAX and PHP.

Working with RSS
RSS is a widely used XML-based standard, used to exchange information between applications on the Internet. One of the great advantages of XML is that it is plain text, thus easily read by any application. RSS feeds can be viewed as plain text files, but it doesn't make much sense to use them like that, as they are meant to be read by specialized software that generates web content based on their data.

While RSS is not the only standard for expressing feeds as XML, we've chosen to use this format in the case study because it's very widely used. In order to better understand RSS, we need to see what lies underneath the name; the RSS document structure, that is.

The RSS Document Structure
The first version of RSS was created in 1999. This is known as version 0.9. Since then it has evolved to the current 2.0.1 version, which has been frozen by the development community, as future development is expected to be done under a different name.

A typical RSS feed might look like this:

<rss version="2.0">
<channel>
<title>CNN.com</title>
<link>http://www.example.org</link>
<description>A short description of this feed</description>
<language>en</language>
<pubDate>Mon, 17 Oct 2005 07:56:23 EDT</pubDate>
<item>
<title>Catchy Title</title>
<link>http://www.example.org/2005/11/catchy-title.html</link>
<description>
The description can hold any content you wish, including XHTML.
</description>
<pubDate>Mon, 17 Oct 2005 07:55:28 EDT</pubDate>
</item>
<item>
<title>Another Catchy Title</title>
<link>http://www.example.org/2005/11/another-catchy-title.html</link>
<description>
The description can hold any content you wish, including XHTML.
</description>
<pubDate>Mon, 17 Oct 2005 07:55:28 EDT</pubDate>
</item>
</chanel>
</rss>

The feed may contain any number of <item> items, each item holding different news or blog entries or whatever content you wish to store.

This is all plain text, but as we stated above, we need special software that will parse the XML and return the information we want. An RSS parser is called an aggregator because it can usually extract and aggregate information from more than one RSS source.

Such an application is Google Reader, an online service from Google, launched in fall 2005. A
veteran web-based RSS reader service is the one at http://www.bloglines.com.

Google Reader
Google Reader (http://reader.google.com) provides a simple and intuitive AJAX-enabled interface that helps users keep track of their RSS subscriptions and reading. It hasn't been long since this service was launched (it's still in beta at the moment of writing), but it has already got a great deal of attention from users. Figure 9.1 shows the Google Reader in action, reading a news item from Packt Publishing's RSS feed.

Figure 9.1: Managing RSS Subscriptions (Feeds) on Google Reader

Implementing the AJAX RSS Reader
In order for this exercise to function correctly, you need to enable XSL support in your PHP
installation. Appendix A contains installation instructions that include XSL support.

In the exercise that will follow we will build our own AJAX-enabled RSS reader application. The main characteristics for the application are:
1. We'll keep the application simple. The list of feeds will be hard-coded in a PHP file on the server.
2. We'll use XSLT to transform the RSS feed data into something that we can display
to the visitor. In this chapter, the XSL transformation will be performed on the server side, using PHP code.
3. We'll use the SimpleXML library to read the XML response from the news server.
SimpleXML was introduced in PHP 5, and you can find its official documentation at http://php.net/simplexml. SimpleXML is an excellent library that can make reading XML sources much easier than using the DOM.

4. The application will look like Figure 9.2:

Figure 9.2: Our AJAX-enabled RSS Reader Start Page

Feeds are loaded dynamically and are displayed as links in the left column. Clicking on a feed will trigger an HTTP request and the server script will acquire the desired RSS feed.

The server then formats the feed with XSL and returns an XML string. Results are then displayed in a human-readable form.

Time for Action—Building the RSS Reader Application
1. In your ajax folder, create a new folder named rss_reader.
2. Let's start with the server. Create a new file named rss_reader.php, and add this code to it:
<?php
// load helper scripts
require_once ('error_handler.php');
require_once ('rss_reader.class.php');
// create a new RSS Reader instance
$reader = new CRssReader(urldecode($_POST['feed']));
// 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');
// return the news to the client echo $reader->getFormattedXML();
?>

3. Create a new file named rss_reader.class.php, and add this code to it:
<?php
// this class retrieves an RSS feed and performs a XSLT transformation
class CRssReader
{
private $mXml;
private $mXsl;

// Constructor - creates an XML object based on the specified feed function __construct($szFeed)
{
// retrieve the RSS feed in a SimpleXML object
$this->mXml = simplexml_load_file(urldecode($szFeed));
// retrieve the XSL file contents in a SimpleXML object
$this->mXsl = simplexml_load_file('rss_reader.xsl');
}

// Creates a formatted XML document based on retrieved feed public function getFormattedXML()
{
// create the XSLTProcessor object
$proc = new XSLTProcessor;
// attach the XSL
$proc->importStyleSheet($this->mXsl);
// apply the transformation and return formatted data as XML string return $proc->transformToXML($this->mXml);
}
}
?>

4. Create a new file named rss_reader.xsl, and add this code to it:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<dl>
<xsl:for-each select="rss/channel/item">
<dt><h3><xsl:value-of select="title" /></h3></dt>
<dd>
<span><xsl:value-of select="pubDate" /></span>
<p>
<xsl:value-of select="description" />
<br />
<xsl:element name="a">
<xsl:attribute name = "href">
<xsl:value-of select="link" />
</xsl:attribute>
read full article
</xsl:element>
</p>
</dd>
</xsl:for-each>
</dl>
</xsl:template>
</xsl:stylesheet>

5. Now add the standard error-handling file, error_handler.php. Feel free to copy this file from the previous chapter. Anyway, here's the code for it:

<?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
if(ob_get_length()) 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;
}
?>

6. In the rss_reader folder, create a file named config.php, where we'll add the feeds our application will aggregate.
<?php
// Set up some feeds
$feeds = array ('0' => array('title' => 'CNN Technology',
'feed' =>
'http://rss.cnn.com/rss/cnn_tech.rss'),
'1' => array('title' => 'BBC News',
'feed' =>
'http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss.xml'),
'2' => array('title' => 'Wired News',
'feed' =>
'http://wirednews.com/news/feeds/rss2/0,2610,3,00.xml'));
?>

7. Create a new file named index.php, and add this code to it:
<?php
// load the list of feeds
require_once ('config.php');
?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>AJAX RSS Reader</title>
<link rel="stylesheet" type="text/css" href="rss_reader.css"/>
<script src="rss_reader.js" type="text/javascript"></script>
</head>
<body>
<h1>AJAX RSS Reader</h1>
<div id="feeds">
<h2>Feeds</h2>
<ul id="feedList">
<?php
// Display feeds
for ($i = 0; $i < count($feeds); $i++)
{
echo '<li id="feed-' . $i . '"><a href="javascript:void(0);" ';
echo 'onclick="getFeed(document.getElementById(\'feed-' . $i .
'\'), \'' . urlencode($feeds[$i]['feed']) . '\');">';
echo $feeds[$i]['title'] . '</a></li>';
}
?>

</ul>
</div>
<div id="content">
<div id="loading" style="display:none">Loading feed...</div>
<div id="feedContainer" style="display:none"></div>
<div id="home">
<h2>About the AJAX RSS Reader</h2>
<p>
The AJAX RSS reader is only a simple application that provides basic functionality for retrieving RSS feeds.
</p>
<p>
This application is presented as a case study in
<a href="https://www.packtpub.com/ajax_php/book"> Building
Responsive Web Applications with AJAX and PHP</a> (Packt Publishing, 2006).
</p>
</div>
</div>
</body>
</html>

8. Create a new file named rss_reader.js, and add this code to it:
// holds an instance of XMLHttpRequest
var xmlHttp = createXmlHttpRequestObject();
// when set to true, display detailed error messages var showErrors = true;

// 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) {} // ignore potential error
}
}
// return the created object or display an error message
if (!xmlHttp)
alert("Error creating the XMLHttpRequest object.");
else

return xmlHttp;
}

// function that displays an error message function displayError($message)
{
// ignore errors if showErrors is false if (showErrors)
{
// turn error displaying Off showErrors = false;
// display error message
alert("Error encountered: \n" + $message);
}
}

// Retrieve titles from a feed and display them function getFeed(feedLink, feed)
{
// only continue if xmlHttp isn't void
if (xmlHttp)
{
// try to connect to the server
try
{
if (xmlHttp.readyState == 4 || xmlHttp.readyState == 0)
{
/* Get number of feeds and loop through each one of them to change the class name of their container (<li>). */
var numberOfFeeds =
document.getElementById("feedList").childNodes.length;
for (i = 0; i < numberOfFeeds; i++)
document.getElementById("feedList").childNodes[i].className = "";
// Change the class name for the clicked feed so it becomes
// highlighted
feedLink.className = "active";
// Display "Loading..." message while loading feed document.getElementById("loading").style.display = "block";
// Call the server page to execute the server-side operation params = "feed=" + feed;
xmlHttp.open("POST", "rss_reader.php", true);
xmlHttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded");
xmlHttp.onreadystatechange = handleHttpGetFeeds;
xmlHttp.send(params);
}
else
{
// if connection was busy, try again after 1 second setTimeout("getFeed('" + feedLink + "', '" + feed + "');", 1000);
}
}
// display the error in case of failure
catch (e)
{
displayError(e.toString());
}
}
}

// function that retrieves the HTTP response function handleHttpGetFeeds()
{

// continue if the process is completed if (xmlHttp.readyState == 4)
{
// continue only if HTTP status is "OK" if (xmlHttp.status == 200)
{
try
{
displayFeed();
}
catch(e)
{
// display error message displayError(e.toString());
}
}
else
{
displayError(xmlHttp.statusText);
}
}
}

// Processes server's response function displayFeed()
{
// read server response as text, to check for errors var response = xmlHttp.responseText;
// server error?
if (response.indexOf("ERRNO") >= 0
|| response.indexOf("error:") >= 0
|| response.length == 0)
throw(response.length == 0 ? "Void server response." : response);
// hide the "Loading..." message upon feed retrieval document.getElementById("loading").style.display = "none";
// append XSLed XML content to existing DOM structure
var titlesContainer = document.getElementById("feedContainer");
titlesContainer.innerHTML = response;
// make the feed container visible document.getElementById("feedContainer").style.display = "block";
// clear home page text
document.getElementById("home").innerHTML = "";
}

9. Create a new file named rss_reader.css, and add this code to it:
body
{
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}

h1
{
color: #ffffff;
background-color: #3366CC;
padding: 5px;
}

h2
{
margin-top: 0px;
}

h3
{

margin-bottom: 0px;
}

li
{
margin-bottom: 5px;
}

div
{
padding: 10px;
}

a, a:visited
{
color: #3366CC;
text-decoration: underline;
}

a:hover
{
color: #ffffff;
background-color: #3366CC;
text-decoration: none;
}

.active a
{
color: #ffffff;
background-color: #3366CC;
text-decoration: none;
}

.active a:visited
{
color: #ffffff;
background-color:#3366CC;
text-decoration:none;
}

.active a:hover
{
color:#ffffff;
background-color: #3366CC;
text-decoration: none;
}

#feeds
{
display: inline;
float: left;
width: 150px;
background-color: #f4f4f4;
border:1px solid #e6e6e6;
}

#content
{
padding-left:170px;
border:1px solid #f1f1f1;
}

#loading
{
float: left;
display: inline;

width: 410px;
background-color: #fffbb8;
color: #FF9900;
border: 1px solid #ffcc00;
font-weight: bold;
}

.date
{
font-size: 10px;
color: #999999;
}

10. Load http://localhost/ajax/rss_reader in your web browser. The initial page should look like Figure 9.3. If you click one of the links, you should get something like Figure 9.2.

Figure 9.3: The First Page of the AJAX RSS Reader

What Just Happened?
It's not a really professional application at this state, but the point is proven. It doesn't take much code to accomplish such a result and any features you might think of can be added easily.

The user interface of this application is pretty basic, all set up in index.php. We first need to include config.php—where our feeds are defined, in order to display the list of feeds on the left panel. Feeds are defined as an associative array of arrays. The main array's keys are numbers starting from 0 and its values are arrays, with keys being the feeds' titles and values being the feeds' URLs. The $feeds array looks like this:

$feeds = array ("0" => array("title" => "CNN Technology",
"feed" => "http://rss.cnn.com/rss/cnn_tech.rss"),

"1" => array("title" => "BBC News", "feed" =>
"http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss.xml"), "2" => array("title" => "Wired News",
"feed" => "http://wirednews.com/news/feeds/rss2/0,2610,3,00.xml"));

Translated into a more meaningful form, this is how the $feeds array looks like:

ID Feed Title (title) Feed URL (feed)

0 CNN Technology

http://rss.cnn.com/rss/cnn_tech.rss

1 BBC News http://news.bbc.co.uk/rss/newsonline_uk_edition/front_page/rss.xml

2 Wired News http://wirednews.com/news/feeds/rss2/0,2610,3,00.xml

We have decided to store the feeds like this for simplicity, but it's easy to extend the code and store them in a database, if you need to.

In index.php we loop through these feeds and display them all as an un-ordered list, each feed being a link inside an <li> element. We assign each link an onclick event function where getFeed function will be called. This function takes two parameters: the <li>'s ID and the feed's URL. We need the ID in order to highlight that link in the list and we need the feed's URL to send it as a parameter in our HTTP request to the server. The urlencode function ensures that the URL is safely sent to the server, which will use urldecode to decode it.

Two more things about index.php:

• Initially hidden, the <div> with id="loading" will be displayed while retrieving the feed, to inform the user that the feed is loading. This is useful when working with a slow connection or with slow servers, when the retrieval time will be long.
<div id="loading" style="display:none">Loading feed...</div>

• The <div> with id="feedContainer" is the actual container where the feed will be loaded. The feed will be dynamically inserted inside this div element.

<div id="feedContainer"></div>

rss_reader.js contains the standard XMLHttpRequest initialization, request sending, and response retrieval code. The getFeed function handles the sending of the HTTP request. First it loops through all feed links and un-highlights the links by setting their CSS class to none. It then highlights the active feed link:

/* Get number of feeds and loop through each one of them to change the class name of their container (<li>). */
var numberOfFeeds =
document.getElementById("feedList").childNodes.length;
for (i = 0; i < numberOfFeeds; i++)
document.getElementById("feedList").childNodes[i].className = "";
// Change the class name for the clicked feed to highlight it feedLink.className = "active";

OK, the next step is to display the Loading feed... message:

// Display "Loading..." message while loading feed document.getElementById("loading").style.display = "block";

And finally, we send the HTTP request with the feed's title as parameter:

// Call the server page to execute the server-side operation params = "feed=" + feed;
xmlHttp.open("POST", "rss_reader.php", true);
xmlHttp.setRequestHeader("Content-Type",
"application/x-www-form-urlencoded"); xmlHttp.onreadystatechange = handleHttpGetFeeds; xmlHttp.send(params);

The rss_reader.php script creates an instance of the CRssReader class and displays an
XSL-formatted XML document, which is returned back to the client. The following lines do the hard work (the code that clears the output and prevents browser caching was stripped):

$reader = new CRssReader(urldecode($_POST['feed']));
echo $reader->getFormattedXML();

CRssReader is defined in rss_reader.class.php. This PHP class handles XML retrieval and formatting. Getting a remote XML file is a piece of cake with PHP 5's new extension: SimpleXML. We'll also load the XSL template and apply it to the retrieved XML.

The constructor of this class retrieves the XML and saves it in a class member named $mXml and the XSL file in a class member named $mXsl:

// Constructor - creates an XML object based on the specified feed function __construct($szFeed)
{
// retrieve the RSS feed in a SimpleXML object
$this->mXml = simplexml_load_file(urldecode($szFeed));
// retrieve the XSL file contents in a SimpleXML object
$this->mXsl = simplexml_load_file('rss_reader.xsl');
}

The getFormattedXML() function creates a new XSLTProcessor object in order to apply the XSL transformation. The transformToXML method simply returns a formatted XML document, after the XSL has been applied.

// Creates a formatted XML document based on retrieved feed public function getFormattedXML()
{
// create the XSLTProcessor object
$proc = new XSLTProcessor;
// attach the XSL
$proc->importStyleSheet($this->mXsl);
// apply the transformation and return formatted data as XML string return $proc->transformToXML($this->mXml);
}

What we need to accomplish with XSL is to loop through each "record" of the XML and display the data inside. A record is delimited by <item> and </item> tags.

In rss_reader.xsl we define a loop like this:

<xsl:for-each select="rss/channel/item">

For example, to display the current title, we write:

<h3><xsl:value-of select="title" /></h3>

Notice how we create a new <a> element with XSLT:

<xsl:element name="a">
<xsl:attribute name = "href">
<xsl:value-of select="link" />
</xsl:attribute>
read full article
</xsl:element>

We use this technique to build links to full articles on their actual websites.

There's also a bit of CSS code that will format the output according to our wish. Everything should be pretty clear if you take a quick look at rss_reader.css.

Summary
Today's Web is different than yesterday's Web and tomorrow's Web will certainly be different than today's. Yesterday's Web was a collection of pages linked together. All static, and everybody kept things for themselves. The main characteristic of today's Web is information exchange between websites and/or applications.

Based on what you've learned in this chapter, you'll be able to build an even better RSS Reader, but why stop here? You hold some great tools that allow you to build great applications that could impact on tomorrow's Web!

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.