| By James Benson, Jay Fienberg | Article Rating: |
|
| May 13, 2007 11:00 AM EDT | Reads: |
5,227 |
This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.
Designing the Server API
At this point, we'll look at the functionality that we need on the server to serve our client. We'll create an API that matches our major service needs, which are:
- Display the client UI
- Return a list of users
- Return messages between the user and each buddy
- Return a list of buddies with their current online status
- Send a new message from the user to a buddy
- Update the user's online status
Displaying the Client UI
Our server is going to live at im.php (in the same directory with the other files: im.html, im.css, and
im.js), and we'll have the default call to http://server/im.php return the contents of im.html. In this
way, the entire interaction with our application will be contained in http://server/im.php.
Returning a List of Users
Because user management is a feature we're only minimally building out in our application, we'll meet our requirement to return a list of users in a simple way that is highly specific to our HTML. We'll just have the server return each user name wrapped in an HTML <option> that can be slotted directly into <select id="user"> in our HTML (see Listing 7).
In our PHP code, we have a function that just writes out the HTML we need:
Listing 12.10a
function printEveryoneAsHTMLOptions($people) {
$out = "<option>***select a user***</option>\n";
foreach ($people as $name) {
$out .= "<option>" . $name ."</option>\n";
}
print $out;
}
XML, HTML, and JSON
In creating our IM server and API before creating the AJAX that uses it, we realized that it was pretty easy on the server-side to provide results in HTML, XML, and JSON formats.
We knew the HTML results would allow us to simplify the development of the AJAX code (which, when using HTML rather than XML, some people call AHAH for "Asychronous HTML and HTTP"). Working with an API that returned HTML results, our JavaScript would be able to directly display them and wouldn't need to parse XML and reformat to match our user interface.
However, we also saw how one could want to introduce other features in the client interface (some of which we'll discuss at the end of the chapter) that would not be easy to do with HTML. We designed our API to return results in either HTML, XML, or JSON.
We also included an extra method that returns "everything" for a user - the status of all of their buddies and all of their messages to and from each buddy.
The Server API (Application Programing Interface)
Here's an overview of the API and samples of the results returned from the server:
Note: in this description, {from} and {to} are placeholders for actual user names in the sending or receiving role, e.g.:
im.php/html/John/buddies
This displays all of John's buddies as HTML. All users (from and to) must be valid as determined by the validFrom() function in im.php. All statuses must be valid as determined by the validStatus() function in im.php.
Listing 12.10b
API Calls:
| ACTION | METHOD | URL |
|---|---|---|
| display client UI | GET | im.php |
| userlist | GET | im.php/userlist |
| {display all data} | GET | im.php/xml/{from} im.php/html/{from} im.php/json/{from} |
| buddies | GET | im.php/html/{from}/buddies im.php/xml/{from}/buddies im.php/json/{from}/buddies |
| msgs | GET | im.php/html/{from}/msgs/{to} im.php/xml/{from}/msgs/{to} im.php/json/{from}/msgs/{to} |
| sendmsg | POST | im.php/sendmsg parameters: msg= text from= user to= user |
| updatestatus | POST | im.php/updatestatus/ parameters: status= validStatus(status) status_msg= text from= user |
Also: add the parameter ?test=true to any URL to get result returned with the text/plain MIME type.
Buddy Results
In Listings 12.10c, 12.10d, and 12.10e, we see the server response to requests for John's buddy statuses in HTML, XML, and JSON, respectively.
Listing 12.10c
request: im.php/html/John/buddies
<div id="for_John">
<div class="budlisting" id="buddy_Paul"><div class="offline"> </div> <p>Paul</p></
div>
<div class="budlisting" id="buddy_George"><div class="offline"> </div> <p>George</
p></div>
<div class="budlisting" id="buddy_Ringo"><div class="offline"> </div> <p>Ringo</
p></div>
<div class="budlisting" id="buddy_Clarence"><div class="offline"> </div>
<p>Clarence</p></div>
</div>
The HTML results are designed to fit into the HTML of our IM client, and we'll also hook event listeners off of the ID attributes so that clicking on a buddy will open a chat window to that buddy (see Listing 12.19).
Listing 12.10d
request: im.php/xml/John/buddies
<?xml version="1.0" encoding="utf-8"?>
<imdata for="John">
<buddies count="4" timestamp="1146708256">
<buddy status="Offline:: Logged Out">Paul</buddy>
<buddy status="Offline:: Logged Out">George</buddy>
<buddy status="Offline:: Logged Out">Ringo</buddy>
<buddy status="Offline:: Logged Out">Clarence</buddy>
</buddies>
</imdata>
Listing 12.10e
request: im.php/json/John/buddies
{
"buddies": {
"count":4,"timestamp":1146708256, "list": [
{"status":"Offline:: Logged Out","name":"Paul"},
{"status":"Offline:: Logged Out","name":"George"},
{"status":"Offline:: Logged Out","name":"Ringo"},
{"status":"Offline:: Logged Out","name":"Clarence"}
]},
"endcap":null}
One thing to note is that the XML and JSON versions include timestamp and count values. These are to aid in parsing on the client side, and the timestamp represents the last time the buddy status was updated on the server. (The JSON results also have an "endcap" value that we throw in there to make different combinations of JSON results easier to generate in the server code.)
Message Results
In Listings 12.10f, 12.10g, and 12.10h, we see the server response to requests for John's message exchange with Paul in HTML, XML, and JSON, respectively.
Listing 12.10f
request: /im.php/html/John/msgs/Paul
<div id="for_John">
<div id="msgs_Paul">
<p>[14:33:09] Paul: howdy</p>
<p>[14:33:15] John: yo!</p>
</div>
</div>
Again, the HTML results are designed to fit into the HTML of our IM client.
Listing 12.10g
request: /im.php/xml/John/msgs/Paul
<?xml version="1.0" encoding="utf-8"?>
<imdata for="John">
<msgs buddy="Paul" timestamp="1146699445" count="34">
<msg timestamp="1146691989">Paul: howdy</msg>
<msg timestamp="1146691995">John: yo!</msg>
</msgs>
</imdata>
Listing 12.10h
request: /im.php/json/John/msgs/Paul
{
"msgs": {
"Paul": {"timestamp":1146699445,"count":34,"list": [
{"timestamp":1146691989,
"msg":'Paul: howdy'},
{"timestamp":1146691995,
"msg":'John: yo!'}
]}
},
"endcap":null}
Again, note that the XML and JSON versions include timestamp and count values. The HTML version includes a formatted time for each message (which matches the needs of our client), but the XML and JSON versions leave the timestamp unformatted to allow more flexibility for other clients.
Overview of the IM Server Code
We'll quickly cover some highlights of the PHP code for our IM server. The code is shown in Listing 12.10i.
As mentioned above, im.php lives in the same directory as the other im.html, im.css, and im.js files. The IM messages and status need to be stored on the server in order to be available to different users, so our PHP code stores these messages in "state" files.
While a more robust approach would be to use a database like MySQL, we again took a simple approach that works well for a small number of users. Similarly, our "user database" is little more than an array of our five, uniquely named, users: John, Paul, George, Ringo, and Clarence.
By default, im.php writes "state" files in the same directory it's stored in, so this directory must be writable by the Web server.
If you have PHP set up on a Web server, you should be able to simply copy the im.* and prototype. js files into a directory, make that directory world writable (e.g., on UNIX and Mac, chmod 777 the directory; on Windows, grant everyone write access to the directory), and then browse to im.php and see all of this code in action.
The files written by im.php store the timestamps of the most recent update to status (statusTS) and new message (messageTS), one file with current status of each user (users), and then each pair of users gets their own file to store their conversations.
In order to create unique IDs for each possible "couple," we do a little trick of creating IDs with the names appended to each other in alphabetical order. For example, all conversations between Ringo and Clarence (regardless of who starts them) live under the ID ClarenceRingo and are stored in a file with the same name.
Also, we chose to store the conversations indefinitely; new messages just keep getting added to the files. If you ever want to "clean slate," you can erase the "state" files at any time.
Finally, near the top of im.php, you'll see there's a $storageDir = ""; statement. You can modify this with a full path to another location for the "state" files. Just be sure to include a final trailing slash on the path.
Listing 12.10i
<?php
/* im.php is a standalone IM client application created
by Jay Fienberg for the Real World Ajax book.
To run this script, copy this file (im.php) with im.html, im.js, and im.css into a PHP-enabled directory on your Web server.
This directory should be writeable by your Web server, because this script saves files, and needs permission to write them.
If you want or need to put the storage files elsewhere, enter the path in the $storageDir variable, below, e.g.:
change $storageDir = ""; to
$storageDir = "/path/to/store/files/";
Be sure to include the trailing slash!
*/
$storageDir = "";
/* setting test=true in the request causes the results to be
returned with the content/type text/plain for debugging
*/
$test = false;
$test = $_REQUEST[‘test'];
/* DATABASE: our imitation user database starts with an array
see the Users function below, which saves user status,
and the getBuddyList function below, which automatically
makes everyonea buddy of everyone else,
and the Msgs function below, which saves messages
*/
$everyone = array("John", "Paul", "George", "Ringo", "Clarence");
/* btw, you can change the user list right here, including
adding more people, e.g., uncomment the next line.
Note: the IM client only supports 4 windows right now
*/
//$everyone = array("Yoko", "Linda", "Pattie", "Barbara", "Ida Mae");
//extract parameter values from POST
$from = $_POST[‘from'];
$to = $_POST[‘to'];
$msg = $_POST[‘msg'];
$status = $_POST[‘status'];
$status_msg = $_POST[‘status_msg'];
//extract actions from the URL
$parts = array();
$f = array_pop(explode("/", __FILE__));
$s = explode("/", $_SERVER["PHP_SELF"]);
$idx = array_search($f, $s);
if ($idx < count($s)) $parts = array_slice($s, $idx+1);
$type = count($parts);
if ($parts[0]=="html" || $parts[0]=="xml" || $parts[0] == "json") {
$mode = array_shift($parts);
$type--;
}
//route to relevant processing and response function
switch ($type) {
case 0: //default: show IM client user interface
include_once("im.html");
exit;
case 1: //userlist, sendmsg, updatestatus, or default all data
if ($parts[0]=="userlist") {
printEveryoneAsHTMLOptions($everyone);
} elseif ($parts[0]=="updatestatus") {
Users(‘set', $from, validStatus($status) . ": " . cleanMsg($status_msg));
} elseif ($parts[0]=="sendmsg" && validUser($from) && validUser($to)) {
Msgs(‘append', uniqueKey($from,$to), $from . ": ". cleanMsg($msg));
} elseif (validUser($parts[0])) { //get all data
startResponse($parts[0], $mode);
printBuddies($parts[0], $mode);
printMsgs($parts[0], $mode);
endResponse($mode);
} else {
returnError("400", $parts[0]);
}
break;
case 2: //get buddies
if (validUser($parts[0]) && $parts[1]=="buddies") {
startResponse($parts[0], $mode);
printBuddies($parts[0], $mode);
endResponse($mode);
} else {
returnError("400", $parts[0]);
}
break;
case 3: //get messages
if (validUser($parts[0]) && $parts[1]=="msgs" && validUser($parts[2])) {
startResponse($parts[0], $mode);
printMsgs($parts[0], $mode, $parts[2]);
endResponse($mode);
} else {
returnError("400", $parts[0]);
}
break;
default: //debug only
print_r($parts);
break;
}
function getBuddyList($user, $DB) {
//imitation buddylist - returns everyone but the user (i.e., $user is $from)
$a_user = array($user);
return array_diff($DB, $a_user);
}
function startResponse($user, $mode="html") {
global $test;
if ($test==true || $mode=="json")
header("Content-type: text/plain");
elseif ($mode=="xml")
header("Content-type: text/xml");
if ($mode=="xml") {
print ‘<?xml version="1.0" encoding="utf-8"?>'."\n";
print ‘<imdata for="' . $user . ‘">' . "\n";
} elseif ($mode=="html") {
print ‘<div id="for_' . $user . ‘">' . "\n";
} else if ($mode=="json") {
print ‘{‘;
}
}
function endResponse($mode="html") {
if ($mode=="xml") print "</imdata>";
elseif ($mode=="html") print "</div>";
elseif ($mode=="json") print "\n" . ‘"endcap":null}';
}
function printEveryoneAsHTMLOptions($people) {
$out = "<option>***select a user***</option>\n";
foreach ($people as $name) {
$out .= "<option>" . $name ."</option>\n";
}
print $out;
}
function printBuddies($user, $mode="html") {
global $everyone;
$buds = getBuddyList($user, $everyone);
$xml = "";
$html = "";
$json = "\n\t" . ‘"buddies": {‘ ."\n";
$xml .= "\t" . ‘<buddies count="' . count($buds) . ‘"';
$json .= "\t\t" . ‘"count":' . count($buds) . ‘,';
$cnt = 0;
foreach ($buds as $name) {
$bud_stat = Users(‘get', $name);
if ($cnt==0) {
$xml .= ‘ timestamp="' . $bud_stat[0] . ‘">' ."\n";
$json .= ‘"timestamp":'. $bud_stat[0] . ‘, "list": [‘ ;
$cnt++;
}
$xml .= "\t\t<buddy status=\"" . $bud_stat[1] . "\">" . $name . "</buddy>\n";
$html .= "\t" . ‘<div class="budlisting" id="buddy_' . $name . ‘"><div class="'.
strtolower(simpleStatus($bud_stat[1])) . ‘"> </div> <p>' . $name . "</p></div>\n";
$json .= "\n\t\t" . ‘{"status":"' . $bud_stat[1] . ‘","name":"' . $name . ‘"},';
}
$xml .= "\t</buddies>\n";
$json = rtrim($json, ‘,') . "\n\t\t]},";
if ($mode=="html") print $html;
elseif ($mode=="xml") print $xml;
elseif ($mode=="json") print $json;
}
function printMsgs($user, $mode="html", $to=null) {
global $everyone;
$buds = getBuddyList($user, $everyone);
$xml = "";
$html = "";
$json = "";
$json .= "\n\t" . ‘"msgs": {‘;
foreach ($buds as $nameforkey) {
$key = uniqueKey($nameforkey, $user);
$keymsgs = Msgs(‘get', $key);
$msgcnt = count($keymsgs[1]);
if ($to==null || ($to!==null && $to==$nameforkey)) {
$json .= "\n\t\t" . ‘"' . $nameforkey . ‘": {‘;
$json .= ‘"timestamp":' . $keymsgs[0] . ‘,';
$json .= ‘"count":'. $msgcnt . ‘,';
$json .= ‘"list": [‘;
if ($msgcnt == 0) {
$xml .= "\t" . ‘<msgs buddy="' . $nameforkey . ‘" timestamp="' . $keymsgs[0]
. ‘" count="0" />' . "\n";
$html .= "\t" .'<div id="msgs_' . $nameforkey . ‘"></div>' ."\n";
} else {
$xml .= "\t" . ‘<msgs buddy="' . $nameforkey . ‘" timestamp="' . $keymsgs[0]
. ‘" count="' . $msgcnt .'">' . "\n";
$html .= "\t" . ‘<div id="msgs_' . $nameforkey . ‘">' ."\n";
foreach ($keymsgs[1] as $im) {
$xml .= "\t\t". ‘<msg timestamp="' . $im[0] . ‘">' . $im[1] . "</msg>\n";
$html .= "\t\t<p>" . "[" . date("H:i:s",$im[0]) . "] " . $im[1] . "</p>\n";
$json .= "\n\t\t\t\t{" . ‘"timestamp":' . $im[0] . ‘,';
$json .= "\n\t\t\t\t" . ‘"msg":\'' . $im[1] . ‘\'},';
}
$xml .= "\t</msgs>\n";
$html .= "\t</div>\n";
$json = rtrim($json, ‘,') . "\n\t\t]},";
}
}
}
$json = rtrim($json, ‘,') . "\n\t},";
if ($mode=="html") print $html;
elseif ($mode=="xml") print $xml;
elseif ($mode=="json") print $json;
}
function cleanMsg($msg) {
$msg = str_replace("[amp]", "&", $msg);
$msg = str_replace(‘"', """, $msg);
$msg = strip_tags($msg);
return $msg;
}
function validStatus($status) {
if (!($status=="Offline"
|| $status=="Away"
|| $status=="Available"))
$status = "Offline";
return $status . ‘:';
}
function simpleStatus($status) {
$comp = explode(":", $status);
return $comp[0];
}
function validUser($name) {
global $everyone;
return in_array($name, $everyone);
}
function returnError($type, $data) {
HTTPStatus($type);
print "Error " . $type . " with ";
print_r($data);
}
//simple message storage database
function Msgs($verb, $noun="", $val="") {
$msgarray = array();
$msgTS = 0;
$test1 = unserialize(getFileContents($noun));
if (!empty($test1)) $msgarray = $test1;
$test2 = unserialize(getFileContents("msgTS"));
if (!empty($test2)) $msgTS = $test2;
switch ($verb) {
case ‘get':
return array($msgTS, $msgarray);
break;
case ‘append':
$t = time();
$msgarray[] = array($t, $val);
if ($msgTS < $t) $msgTS = $t;
setFileContents($noun, serialize($msgarray));
setFileContents("msgTS", serialize($t));
return $t;
break;
case ‘delete':
$msgarray = null;
$t = time();
if ($msgTS < $t) $msgTS = $t;
setFileContents($noun, serialize($msgarray));
setFileContents("msgTS", serialize($t));
return $t;
break;
}
}
//simple user and status storage database
function Users($verb, $noun="", $val="") {
$defaultStatus = "Offline:";
$statusarray = array();
$statusTS = 0;
$test1 = unserialize(getFileContents("users"));
if (!empty($test1)) $statusarray = $test1;
$test2 = unserialize(getFileContents("statusTS"));
if (!empty($test2)) $statusTS = $test2;
switch ($verb) {
case ‘get':
if (!isset($statusarray[$noun])) $statusarray[$noun] = $defaultStatus;
return array($statusTS, $statusarray[$noun]);
break;
case ‘set':
$statusarray[$noun] = $val;
$t = time();
if ($statusTS < $t) $statusTS = $t;
setFileContents("users", serialize($statusarray));
setFileContents("statusTS", serialize($t));
return $t;
break;
case ‘delete':
if (isset($statusarray[$noun])) $statusarray[$noun] = null;
$t = time();
if ($statusTS < $t) $statusTS = $t;
setFileContents("users", serialize($statusarray));
setFileContents("statusTS", serialize($t));
return $t;
break;
}
}
function uniqueKey($n1, $n2) {
/* this is a simple way to generate a unique key for
a conversation between two people in this app -
each key is just the two names put together in alpha order
*/
return ($n1 < $n2) ? $n1 . $n2 : $n2 . $n1;
}
function getFileContents($key) {
$contents = "";
$handle = fopen($storageDir . $key, ‘r');
if ($handle && filesize($key) > 0 ) {
$contents = fread($handle, filesize($key));
fclose($handle);
} elseif (!$handle) {
setFileContents($key, ‘');
}
return $contents;
}
function setFileContents($key, $content) {
$handle = fopen($storageDir . $key, ‘w');
$contents = fwrite($handle, $content);
fclose($handle);
return $contents;
}
function HTTPStatus($num) {
static $http = array (
100 => "HTTP/1.1 100 Continue",
101 => "HTTP/1.1 101 Switching Protocols",
200 => "HTTP/1.1 200 OK",
201 => "HTTP/1.1 201 Created",
202 => "HTTP/1.1 202 Accepted",
203 => "HTTP/1.1 203 Non-Authoritative Information",
204 => "HTTP/1.1 204 No Content",
205 => "HTTP/1.1 205 Reset Content",
206 => "HTTP/1.1 206 Partial Content",
300 => "HTTP/1.1 300 Multiple Choices",
301 => "HTTP/1.1 301 Moved Permanently",
302 => "HTTP/1.1 302 Found",
303 => "HTTP/1.1 303 See Other",
304 => "HTTP/1.1 304 Not Modified",
305 => "HTTP/1.1 305 Use Proxy",
307 => "HTTP/1.1 307 Temporary Redirect",
400 => "HTTP/1.1 400 Bad Request",
401 => "HTTP/1.1 401 Unauthorized",
402 => "HTTP/1.1 402 Payment Required",
403 => "HTTP/1.1 403 Forbidden",
404 => "HTTP/1.1 404 Not Found",
405 => "HTTP/1.1 405 Method Not Allowed",
406 => "HTTP/1.1 406 Not Acceptable",
407 => "HTTP/1.1 407 Proxy Authentication Required",
408 => "HTTP/1.1 408 Request Time-out",
409 => "HTTP/1.1 409 Conflict",
410 => "HTTP/1.1 410 Gone",
411 => "HTTP/1.1 411 Length Required",
412 => "HTTP/1.1 412 Precondition Failed",
413 => "HTTP/1.1 413 Request Entity Too Large",
414 => "HTTP/1.1 414 Request-URI Too Large",
415 => "HTTP/1.1 415 Unsupported Media Type",
416 => "HTTP/1.1 416 Requested range not satisfiable",
417 => "HTTP/1.1 417 Expectation Failed",
500 => "HTTP/1.1 500 Internal Server Error",
501 => "HTTP/1.1 501 Not Implemented",
502 => "HTTP/1.1 502 Bad Gateway",
503 => "HTTP/1.1 503 Service Unavailable",
504 => "HTTP/1.1 504 Gateway Time-out"
);
header($http[$num]);
}
?>
Building Out the AJAX
In order to highlight the JavaScript/AJAX parts of our applicaiton, we'll present the JavaScript as if it's being written to connect preexisting HTML/CSS and PHP server code, e.g., everything presented earlier in this chapter.
We did, in fact, create the JavaScript after having developed much of the user interface and the server-side API. But, it would be misleading to suggest that there wasn't adjustments going on between the HTML, CSS, JavaScript, and PHP throughout this application's development. For example, as we developed our class structure in the JavaScript, we added new features to the HTML structure, new style rules to the CSS, and made adjustments to result format returned by the PHP.
This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.
Published May 13, 2007 Reads 5,227
Copyright © 2007 SYS-CON Media, Inc. — All Rights Reserved.
Syndicated stories and blog feeds, all rights reserved by the author.
More Stories By James Benson
Jim Benson, AICP, is the COO of Gray Hill Solutions in Seattle. Gray Hill creates tools for government and industry to harness and utilize real-time data. Jim has always driven applications for his clients to store and provide information in easily extensible ways. Web 2.0 has therefore been a natural environment for him. He is also involved with the Cooperation Commons and the Institute for the Future's Future Commons to study human cooperation and envision the future of cooperation. Jim's tags: Gray Hill Solutions (www.grayhillsolutions.com), Jim's Blog (http://ourfounder.typepad.com), Cooperation Commons (www.cooperationcommons.org), Institute for the Future (www.iftf.org).
More Stories By Jay Fienberg
Jay Fienberg is co-founder of Juxtaprose where he designs information architecture and user experience for websites and information systems.
He specializes in design for content and information-rich websites and web-based social and collaboration systems. His current preferred CMS is ExpressionEngine, though he also works with Wordpress and Joomla, and still gets called upon to make SharePoint do interesting CMS-like things for enterprise intranets.
Since the early 1990s, Jay also has designed and developed hypertext, database, and content management systems and worked in a wide range of programming languages including XML, SQL, SGML, Python, PHP, Javascript, Java, HTML, CSS and APL.
For anything more official, please visit jayfienberg.com.
- Practical Approaches for Optimizing Website Performance
- SQL Anywhere Server and AJAX
- The Difference Between Web Hosting and Cloud Computing
- Ajax in RichFaces 3.3, JSF 2 and RichFaces 4
- Cloud Computing on Gartner's Top 10 List and SYS-CON Events' 2010 Calendar
- IBM Hardware Chief, Intel VC Exec Arrested in Insider Trading Scam
- US Post Office Hops a Ride on NetSuite’s Cloud
- Gang of Four Creates Cloud BI Stack
- Tactical Cloud Computing Panel at 1st Annual GovIT Expo
- Confessions of a Ulitzer Addict
- AJAX World RIA Conference & Expo Kicks Off in New York City
- An Introduction to Abbot
- What is Web 3.0?
- AJAXWorld RIA Conference & Expo 2009 West: Call for Papers
- Interviewing Java Developers With Tears in My Eyes
- Adobe Enters Cloud Computing with LiveCycle
- REA Is Where RIA Becomes the Norm
- RIAs for Web 3.0 Using the Microsoft Platform
- Practical Approaches for Optimizing Website Performance
- Social Media Terrorists
- Building a Drag-and-Drop Shopping Cart with AJAX
- What Is AJAX?
- Google Maps! AJAX-Style Web Development Using ASP.NET
- Flashback to January 2006: Exclusive SYS-CON.TV Interviews on "OpenAjax Alliance" Announcement
- AJAXWorld Conference & Expo to Take Place October 2-4, 2006, at the Santa Clara Convention Center, California
- AJAX Sponsor Webcasts Are Now Available at AJAXWorld Website
- How and Why AJAX, Not Java, Became the Favored Technology for Rich Internet Applications
- "Real-World AJAX" One-Day Seminar Arrives in Silicon Valley
- AJAXWorld University Announces AJAX Developer Bootcamp
- AJAX Support In JadeLiquid WebRenderer v3.1
































