Feature Story : Learning AJAX
Published 22 Nov 2005 ~ revised 20 Feb 2006
Please make note of the changes I've made to this article, and to my AJAX handling.
Those of us in the web development world have not been able to go anywhere on the net recently without hearing about "Asynchronous JavaScript and XML," or AJAX. I will leave the detailed description and definition to the Wikipedia gurus, but the basics are as follows:
Ajax is a web development technique for creating interactive web applications using a combination of:- Wikipedia, "AJAX"
- XHTML (or HTML) and CSS for presenting information
- The Document Object Model manipulated through JavaScript to dynamically display and interact with the information presented
- The XMLHttpRequest object to exchange data asynchronously with the web server. (XML is commonly used, although any format will work, including preformatted HTML, plain text, JSON and even EBML)
In English, that means you can do a mess of operations in the background without modifying the initial page load. One of the best and most elegant examples of this is Google Maps. Obviously an entire country's worth of map data is not sent to your browser every time you zoom in on your street. Google only shows what is relevant to you in the map space of the web page, without changing any other part of the initial layout. Netflix also uses AJAX to pull extended movie data (like plot summaries and cast) on each poster mouse-over.
So I recently decided to get over my hatred of JavaScript and give this a try. The trouble is there are only a handful of remotely lucid tutorials on the subject, being such a relatively new development technique. Thankfully, I found Rasmus' 30 second AJAX Tutorial and Bill Bercik's Guide to Using AJAX and XMLHttpRequest - two extremely good articles on learning the basics. I will not repeat all their work here, but I found both to be invaluable with my own PHP-based implementation.
I decided to do a Netflix-style enhancement to Dynamic Drive's Tooltip II tooltip/popout code that I use for any items that link back to Amazon on my site. My goal was to mouseover a link to a movie, CD, or book and use AJAX to query Amazon's Web Services for extended product information. This info would then appear within the tooltip. So let's get down to business.
The first step is to create the request object which initiates the asynchronous call to my webserver. This is
wrapped in a <script> tag and included either inline or by file reference.
function createRequestObject() {
var ro;
var browser = navigator.appName;
if (browser == "Microsoft Internet Explorer") {
ro = new ActiveXObject("Microsoft.XMLHTTP");
} else {
ro = new XMLHttpRequest();
}
return ro;
}
var http = createRequestObject();
Notice how Internet Explorer likes to be special and use ActiveX for their request objects? Moving along, next I create a function using the request object to make the call to Amazon.
function awsReq(asin,stars,review) {
var awsUrl = "awsget.php?jsasin="+asin+"&jsstars="+stars+"
&jsreview="+review;
http.open('GET', awsUrl);
http.onreadystatechange = handleAWSResponse;
http.send(null);
return '<p align="center"><em><strong>Please wait...</strong>
<br>Retrieving product data from Amazon.com.</em></p>';
}
There are a few things to notice here. First, after spending hours online I learned that an XMLHTTPRequest cannot
make calls to external servers. Therefore, I had to write a quick PHP script to get Amazon's data for me and
return it in an XML document that the request object could understand. That code is below. Also notice that
this request function returns a string that is displayed until the response function can throw back something
meaningful. Anyways, here is my PHP script. I had to throw in some linebreaks below for readability, and I
removed my own Amazon subscription and associate IDs. You will also notice that I am inserting two of my own
XML tags before sending it back to the request function for processing. More on that later. Finally, since
I must go out to Amazon to get this data, speed is a major concern. So the first time I make the request I
save the XML file locally; that way, every other request after that is simply made to a local file instead of
out to Amazon and back every time.
if ($_GET['jsasin'] && $_GET['jsstars'] && $_GET['jsreview']) {
$asin = $_GET['jsasin'];
$stars = $_GET['jsstars'];
$review = $_GET['jsreview'];
$asin = substr(strip_tags(stripslashes($asin)), 0, 20);
$stars = substr(strip_tags(stripslashes($stars)), 0, 2);
$review = substr(strip_tags(stripslashes($review)), 0, 100);
$awsfile = "/[path to writeable directory]/" . $asin . ".xml";
if (file_exists($awsfile)) {
$url = $awsfile;
$xmlfile = @fopen($url, "r")
or die ("<p>Error retrieving wishlist from Amazon.
<p>Please contact webmaster@scottahearn.com<p>");
$readfile = fread($xmlfile, 40000);
} else {
$url = "http://webservices.amazon.com/onca/xml
?Service=AWSECommerceService&Version=2005-03-23
&Operation=ItemLookup&ContentType=text%2Fxml
&SubscriptionId=[my subscription id]
&AssociateTag=[my associate tag]
&ItemId=" . $asin . "
&ResponseGroup=Images,ItemAttributes";
$xmlfile = @fopen($url, "r") or die
("<p>Error retrieving wishlist from Amazon.<p>Please
contact webmaster@scottahearn.com<p>");
$readfile = fread($xmlfile, 40000);
$pattern = "/<\/ItemLookupResponse>/i";
$replacement = "<MyRating>" . $stars . "</MyRating>\n<MyReview>"
. $review . "</MyReview>\n</ItemLookupResponse>";
$readfile = preg_replace($pattern, $replacement, $readfile);
$handle = @fopen($awsfile, "w") or die ("<p>Error opening file
for writing.");
if (fwrite($handle, $readfile) === FALSE) {
echo "Cannot write to file ($awsfile)";
exit;
}
fclose($handle);
}
fclose($xmlfile);
header("Content-type: text/xml");
echo $readfile;
}
On to the response function. This takes the data retrieved by the request, formats it so it's usable to me, and sends it to the caller, which in my case is the DIV associated with the tooltip.
function handleAWSResponse() {
if(http.readyState == 4) {
if (http.responseText.indexOf('invalid') == -1) {
var xmlDocument = http.responseXML;
var mediumimg = xmlDocument.getElementsByTagName('URL')[1]
.firstChild.data;
var title = xmlDocument.getElementsByTagName('Title')[0]
.firstChild.data;
var releasedate
= xmlDocument.getElementsByTagName('ReleaseDate')[0]
.firstChild.data;
var trimrelease = releasedate.substring(0,4);
var myrating = xmlDocument.getElementsByTagName('MyRating')
[0].firstChild.data;
var myreview = xmlDocument.getElementsByTagName('MyReview')
[0].firstChild.data;
document.getElementById('dhtmltooltip').innerHTML =
'<img src="'+mediumimg+'" align="right">
<strong>'+title+'</strong><br>('+trimrelease+')
<p><em><strong>ScottAHearn.com says:</strong></em>
<br><img src=/images/stars_2_'+myrating+'0.gif>
<br><em>'+myreview+'</em>';
}
}
}
As you can see, I'm pulling out certain XML elements by name. For clarification, the URL[1] is the second
occurance of the URL tag in Amazon's XML document. This corresponds to their "medium" image/cover-art. Zero (0)
is the small image, two (2) is the large image. The last line of the function is telling JavaScript to take
all this data and populate the "dhtmltooltip" DIV. Formatting is to my own taste. There is more that can be
done with this function, particularly with the request return codes and state. I am doing redimentary checking
of "readyState == 4" to make sure something complete is coming my way, and verifying the integrity
of the "responseText".
Finally, the caller itself is as follows.
<a href="http://www.amazon.com/exec/obidos/ASIN/B000BB1MI2/[my Amazon
associate ID goes here]"
onmouseover="ddrivetip(awsReq('B000BB1MI2','4','My review!'));"
onmouseout="hideddrivetip();">
<em>Charlie and the Chocolate Factory</em></a>
The "ddrivetip" functions are part of the tooltip generator. You can feed those anything that you want, as
I do in other places within my site. In this case, I am sending it my request function, with corresponding
arguments for the Amazon ASIN, my own rating (out of 5), and my own review. Tie it all together and here is
the result:
Charlie and the Chocolate Factory
That is the gist of it, but I still ran into some trouble spots that are worth pointing out. First,
the request is made immediately. Since I'm querying an outside server, latency causes a brief delay
in the response output. If the mouseover event doesn't complete in its entirety before the mouse is
taken off the entity (in this case, a hyperlink), there are JavaScript errors galore. Therefore, I
used Bercik's "isWorking" code to catch unfinished requests cleanly. See the following
paragraph and code snippet.
Finally, since I reuse the tooltip code in different ways throughout my site (without always wanting to call the Amazon code), I had to be able to control the width and height of the tooltip. This step was essential to me because Internet Explorer in particular needs the dimensions up front or the tooltip will fly out of the viewable area. Therefore, I had to put in some stylesheet overrides within my request function.
var isWorking = false;
function awsReq(asin,stars,review) {
if (!isWorking && http) {
document.getElementById('dhtmltooltip').style.width = '300px';
document.getElementById('dhtmltooltip').style.height = '175px';
document.getElementById('dhtmltooltip').style.textAlign = 'left';
...
http.onreadystatechange = handleAWSResponse;
isWorking = true;
http.send(null);
...
}
}
function handleAWSResponse() {
if(http.readyState == 4) {
if (http.responseText.indexOf('invalid') == -1) {
...
isWorking = false;
}
}
}
Download the tooltip code yourself
or see my own modifications of it [JavaScript and CSS].
You can also view the finished JavaScript behind the AJAX here.
Update, 20 Feb 2006: I now have database access on my host server, so am no longer pulling data from local XML files retrieved from Amazon. The only significant changes are:
- Until I can write a utility to do it, I manually put Amazon data (including my own reviews and ratings) into my own database.
- My PHP retrieval script,
awsget.php. This receives a row id from the caller, pulls all related information from the database, and returns it in an XML format that my JavaScript parser function can deal with (as before). - Any hyperlink (to call the data) now only needs to send a row id instead of the ASIN and all that other nonsense I did before.

