More dynamic websites thanks to AJAX

AJAX Power


AJAX technology adds dynamic elements to enhance sluggish websites. All it takes is a server-side Perl program and some client-side JavaScript code.

By Michael Schilli

Web developers were rudely awakened when Google introduced its Maps service. All of a sudden users could move maps dynamically, as though the application were running as a local GUI rather than in a browser. All of a sudden, time-consuming client-server round trips were hardly noticeable, since the current page didn't need to be reloaded in order to reflect state changes in the application. Today, Ajax applications are sprouting all over the web. The beta release of Yahoo! Webmail, for example, looks very much like a desktop application; you have to take a very close look to see that your web browser is running the show.

AJAX (Asynchronous JavaScript and XML) is based on dynamic HTML and client-side JavaScript. The XMLHttpRequest object, originally added by Microsoft and flying under the radar until Google helped it to fame, allows a JavaScript script downloaded from a website to exchange data asynchronously with the web server. It then dynamically smuggles this data into the HTML page, meaning that only minor changes need to take place on the page.

Figure 1 shows a sample application that manages text snippets commonly used in email and serves them up in a text field for cutting and pasting. You can select Add new topic to add a new text snippet to be stored on the server. Update sends the corresponding text to the server and Remove deletes the selected entry.

Figure 1: The text snippet manager in a browser.

Figure 2: Communication between the browser, the JavaScript engine, and the web server.

The page is loaded just once by the browser. The underlined links may look like hyperlinks, and they are clickable, but they won't cause the browser to jump to a new URL. Instead, they simply execute the JavaScript code specified by the OnClick handler, talking to the server behind the scenes.

The CGI::Ajax Perl module by Brent Pedersen makes this mechanism quite simple to implement. The module defines a client-server protocol (in JavaScript and Perl), which the client-side JavaScript code can use to call server-side Perl functions by referencing their names and parameter lists. A XMLHttpRequest (ActiveXObject("Microsoft.XMLHTTP") on IE) JavaScript object enables the browser to sends a GET request to the server-side CGI script. The request triggers a previously specified Perl function. which returns one or more values. The JavaScript code picks them up and refreshes predefined fields in the browser GUI.

The CGI::Ajax object created by CGI::Ajax->new(`display' => \&display, ...); in the CGI script ensures that the HTML sent back will contain a JavaScript section. The web application uses a JavaScript function named display to fill the browser's text area with the currently selected text snippet. Correspondingly, a Perl function named display() function is defined on the server; the JavaScript handler later calls it using an HTTP request. A radio button defined in HTML with the OnClick handler, OnClick="display([`Lame Excuse'], [`tarea', `statusdiv'])", calls the JavaScript function display() and passes the two specified arrays to the function. The first array contains the id attribute of the selected radio button; its text value (Lame Excuse) gets passed to the server function. The second array contains the id attributes of the HTML tags, which the handler refreshes with the return value from the server function after completing the request. This way, both the text area and the status text field get updated with fresh server data.

In our case, the radio button with the id "Lame Excuse" also has the property VALUE="Lame Excuse", meaning that the server-side Perl display() function (line 12 in Listing snip) is passed this text string as its first parameter. display() does nothing apart from retrieving the text block to match "Lame Excuse" from the server-side cache and sending it back to the browser along with a status message. This is where the JavaScript event handler cuts in again, refreshing the large text field (id=`tarea') and the status field down at the bottom (id=`statusdiv') with the strings returned by display(). All of this is neatly abstracted by CGI::Ajax, which sends the required JavaScript code to the browser, and sets up the server-side handler for accessing the Perl functions.

Client-side, snip does more than just refreshing text fields. If the user deletes one of the headings by clicking on Remove, this not only deletes the radio button with the heading, but also selects the first heading in the remaining list and loads the matching text block from the server. CGI::Ajax can't handle client-side trickery like this yet, but additionally defined JavaScript functions will help.

On a positive note, CGI::Ajax is extremely easy to use. As Listing 1 shows, you only need to define functions for the various client actions (remove/update/display) and provide a show_html function, which returns the client application HTML on initial loading.

Listing 1: snip
01 #!/usr/bin/perl -w
02 use strict;
03 use CGI;
04 use CGI::Ajax;
05 use Cache::FileCache;
06 use Template;
07
08 my $cache =
09   Cache::FileCache->new();
10
11 #############################
12 sub display {
13 #############################
14  my ($topic) = @_;
15
16  return $cache->get($topic),
17    "Retrieved $topic";
18 }
19
20 #############################
21 sub remove_me {
22 #############################
23  my ($topic) = @_;
24
25  $cache->remove($topic);
26  return "Deleted $topic";
27 }
28
29 #############################
30 sub update_me {
31 #############################
32  my ($topic, $text) = @_;
33
34  $cache->set($topic, $text);
35
36  my $disptext = $text;
37  $disptext =
38    substr($text, 0, 60)
39    . "..."
40    if length $text > 60;
41  return
42    "Topic '$topic' updated "
43    . "with '$disptext'";
44 }
45
46 #############################
47 sub show_html {
48 #############################
49  my $template =
50    Template->new();
51
52  my @keys =
53    sort $cache->get_keys();
54
55  $template->process(
56   "snip.tmpl",
57   { topics => \@keys },
58   \my $result)
59    or die $template->error();
60
61  return $result;
62 }
63
64 #############################
65 # main
66 #############################
67 my $cgi = CGI->new();
68 $cgi->charset("utf-8");
69
70 my $pjx = CGI::Ajax->new(
71  'display'   => \&display,
72  'update_me' => \&update_me,
73  'remove_me' => \&remove_me
74 );
75 print $pjx->build_html($cgi,
76  \&show_html);

HTML and Perl Separated

The snip Perl script takes the HTML to be sent from the snip.tmpl template, which is shown in Figure 3. The template toolkit loads the template and provides a number of constructs that let you embed simple for loops or conditions.

Figure 3: The HTML template that snip processes using the template toolkit.

It uses [% FOREACH topic = topics %] to iterate over the @topics array previously provided by snip with the text block headings and outputs a number of radio buttons, each in a separate table row. [% topic %] returns the value of the topic template variable each time. The id property of each radio button is set to the text string in the heading, and the OnClick handler calls the display() function described previously, which exists both in the client-side JavaScript and in the Perl universe on the server.

The select_first() function selects the first entry in the list of radio buttons and requests the text block for the appropriate heading. It first calls the method document.getElementById to search for the HTML tag with the ID of form (the HTML form in snip.tmpl). All radio buttons are named r, so form.r should be an array, holding entries for each radio button found. If the list of headings is empty, select_first() obviously selects nothing; nor does it talk to the server.

The tag type <A> hyperlinks used in snip should have return false as the last action in their OnClick handlers. This ensures that the browser executes the JavaScript code assigned to the link, rather than following the bogus HREF attribute.

The template loads the JavaScript library snip.js first (Listing 2); the library provides a number of functions to allow the GUI to run properly. The JavaScript topic_add() function in snip.js expects the string from a heading and appends a new entry with this name to the end of the radio button table.

Listing 2: snip.js
001 // ##########################
002 function topic_add(topic) {
003 // ##########################
004   var itemTable = document.getElementById("topics");
005   var newRow    = document.createElement("TR");
006   var newCol1   = document.createElement("TD");
007   var newCol2   = document.createElement("TD");
008   var input     = document.createElement("INPUT");
009
010   if(topic.length == 0) {
011       alert("No topic name specified.");
012       return false;
013   }
014
015   input.name    = "r";
016   input.type    = "radio";
017   input.id      = topic;
018   input.value   = topic;
019   input.onclick = function() {
020       display([topic], ['tarea', 'statusdiv']);
021   };
022   input.checked = 1;
023   newCol1.appendChild(input);
024
025   var textnode = document.createTextNode(topic);
026   newCol2.appendChild(textnode);
027
028   itemTable.appendChild(newRow);
029   newRow.appendChild(newCol1);
030   newRow.appendChild(newCol2);
031
032   document.getElementById ('tarea').value = "";
033   document.getElementById ('new_topic').value = "";
034
035   return false;
036 }
037
038 // ##########################
039 function topic_update() {
040 // ##########################
041   if(!id_selected()) {
042     alert("Create a new topic first");
043     return;
044   }
045   update_me( [ id_selected(), 'tarea' ],
046              'statusdiv');
047 }
048
049 // ##########################
050 function topic_remove() {
051 // ##########################
052   var sel = id_selected();
053
054   if(!sel) { alert("No topic available");
055     return;
056   }
057
058   remove_me([sel], 'statusdiv');
059
060   var node = document.getElementById(sel);
061   var row  = node.parentNode.parentNode;
062   row.parentNode.removeChild(row);
063   select_first();
064 }
065
066 // ##########################
067 function select_first() {
068 // ##########################
069   var form = document.getElementById("form");
070   if(! form.r) { return; }
071   if(! form.r.length) {
072     form.r.checked = 1;
073     if(! document.getElementById(id_selected()) ) {
074        document.getElementById ('tarea').value = "";
075       return;
076     }
077     display([id_selected()], ['tarea', 'statusdiv']);
078   }
079
080   for(var i = 0; i < form.r.length; i++) {
081     form.r[i].checked = 1;
082     break;
083   }
084   display([id_selected()], ['tarea', 'statusdiv']);
085 }
086
087 // ##########################
088 function id_selected() {
089 // ##########################
090   sel = id_selected_first_pass();
091
092   if(! document.getElementById(sel) ) {
093     document.getElementById ('tarea').value = "";
094     return;
095   }
096   return sel;
097 }
098
099 // ##########################
100 function id_selected_first_pass() {
101 // ##########################
102   var form = document.getElementById("form");
103   if(! form.r) { return 0; }
104   if(! form.r.length) { return form.r.id; }
105
106   for(var i = 0; i < form.r.length; i++) {
107     if(form.r[i].checked) {
108       return form.r[i].id;
109     }
110   }
111   alert("Selected ID is unknown");
112   return 0;
113 }

topic_remove() deletes a heading from the list of radio buttons and sends a request to the server, which in turn deletes the text snippet from the cache.

id_selected() outputs the id property of the selected radio button, that is, the heading of the entry the user wants to see. If the list is empty, Firefox can become confused and still return a value. To work around this bug, id_selected() rechecks the result of id_selected_first_pass() and returns undefined if it catches Firefox cheating.

If the list of radio buttons has two or more entries, form.r.length returns the list size. If the list only has a single entry, form.r.length returns an undefined value. If the list is empty, form.r is undefined. The checked flag can then either be verified via form.r.checked, or via element "i" in the array: form.r[i].checked. After this, select_first() then calls the display() function to get the text for the selected heading from the server.

Rough Edges

This script is only intended as an example. To keep it simple, code for handling error conditions has been left out of the script. You might experience compatibility problems with browsers other than Firefox.

Installation

The script requires the Class::Accessor, CGI::Ajax, and Template modules from CPAN. Then add the snip script (executable!) and the snip.tmpl template (in the cgi-bin on the web server) and the snip.js JavaScript script directly below the document root (typically htdocs).

If the terminal used for cutting and pasting does not speak UTF-8, you might like to comment out line 68 in snip to tell the web server to send charset=iso-8859-1 in the CGI header instead. And if you prefer not to have the document cache reside below /tmp, you will need to specify your preferred directory by setting my $cache = Cache::FileCache->new({cache_root => "/path"}) in line 9. Finally, point your browser to http://server/cgi-bin/snip, and - assuming that JavaScript is enabled - the text snippet function should run, talk to the server, and keep the snippet repository on the server up to date.

INFO
[1] Listings for this article: http://www.linux-magazine.com/Magazine/Downloads/62/Perl