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:
  • 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)
- Wikipedia, "AJAX"

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:

  1. Until I can write a utility to do it, I manually put Amazon data (including my own reviews and ratings) into my own database.
  2. 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).
  3. 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.
So no major changes in the JavaScript department, only with the data retrieval aspects. View the source code to this page to see the change to the caller, and the JavaScript for the AJAX. You can see my updated PHP retrieval script here.