Tutorial 3: Creating new widgets with AJAX

Last modified: May 15, 2006 - 18:32

AJAX is a catchy acronym used for Javascript applications that dynamically load data from a server, enabling the updating of content without fully refreshing a page.

In Drupal, AJAX functionality is provided through functions in the Javascript file drupal.js. Different web browsers handle client-server communications differently; these functions provide cross-browser methods.

Some AJAX widgets (autocomplete, progress) ship with Drupal, see Tutorial 2.

How you build your own AJAX widgets will of course depend a lot on what you're wanting to do. But here are some basic steps to get you started.

AJAX solution components

At their most basic, AJAX widgets will have three components.

  • Caller
    Page element(s) that call an AJAX update, generally as a result of a user action (click, mouseover, etc.).
  • PHP handler
    A PHP function that receives the user input and returns data to the caller.
  • JS handler
    A Javascript function that receives the PHP response and acts on it (e.g., updates user display).

Example: click_info with AJAX

In Tutorial 1 we coded a stunningly useless module for popping up messages when users click on select words. Here, we'll extend that example to load dynamically the message text from the server using - you guessed it - AJAX.

If you haven't already read it, you might want to glance over Tutorial 1 first.

So here it is, AJAX in three easy steps.

1. Marking up content

As with non-AJAX Javascripting, we want to begin with plain HTML elements, to which behaviours will be attached dynamically after the page loads. The only difference here is that the elements need to have a way of directing the Javascript to the appropriate path on the server. That is, they need to pass to the Javascript the path to post requests to, which will lead to the PHP handler (see below).

Let's call the path of our PHP handler click_info_ajax/example_handler.

In Tutorial 1 we wrote a function that added a CSS class selector and a special info attribute to span elements.

<?php
function click_info_make($text, $words) {
  foreach (
$words as $word => $message) {
   
$text = str_replace ($word, '<span class="clickInfo" t="' . $message . '">' . $word . '</span>', $text);
  }
  return
$text;
}
?>

Here we'll do something similar, only we'll attach the PHP handler path instead, with the word appended to the uri (so it can later be fed into the Javascript as an argument). Note that we generate the uri with url(), so that it will work whether or not clean urls are enabled. (We're using substr_replace() instead of str_replace() so the behaviour will be attached only to the first occurrence of the word.)

<?php
/**
* Add spans to words.
*/
function click_info_ajax_make($text, $words) {
  foreach (
$words as $word) {
   
$pos = strpos($text, $word);
    if (
$pos === false) {
      continue;
    }
   
$text = substr_replace($text, '<span class="clickInfo" title="' . url('click_info_ajax/example_handler/' . $word) . '">' . $word . '</span>', $pos, strlen($word));
  }
  return
$text;
}
?>

Now the Javascript will have what it needs to post a request to the server.

2. The Javascript

Step 2 is writing Javascript to post information to the server, and to interpret the response.

Objects and methods

To attach our new behaviour, we're going to create a new object type, "click info data base" (CIDB, for short) and assign an object instance to our page element. The AJAX functionality will be methods of this new object. This approach might sound complicated at first, but, don't worry, it's actually straightforward.

We'll start with the click_info.js file we wrote in Tutorial 1.

if (isJsEnabled()) {
  addLoadEvent(clickInfoAutoAttach);
}

function clickInfoAutoAttach() {
  var spans = document.getElementsByTagName('span');
  for (var i = 0; span = spans[i]; i++) {
    if (span && hasClass(span, 'clickInfo')) {
      // Read in the message from the 'title' attribute
      span.message = span.getAttribute('title');
      // Remove the title, so no tooltip will display
      span.removeAttribute('title');
      span.onclick = function() {
        alert(this.message);
      }
    }
  }
}

In that case we were popping up text loaded from the element itself. Now, we'll get the data from the server instead. So, instead of attaching a behaviour directly to the element (a span), we'll create a new object type and use that object to do both the behaviour attaching and the AJAX handling.

if (isJsEnabled()) {
  addLoadEvent(clickInfoAutoAttach);
}

function clickInfoAutoAttach() {
  var cidb = [];
  var spans = document.getElementsByTagName('span');
  for (var i = 0; span = spans[i]; i++) {
    if (span && hasClass(span, 'clickInfo')) {
      // Read in the path to the PHP handler
      uri = span.getAttribute('title');
      // Remove the title, so no tooltip will display
      span.removeAttribute('title');
      // Create an object with this uri. Because
      // we feed in the span as an argument, we'll be able
      // to attach events to this element.
      if (!cidb[uri]) {
        cidb[uri] = new CIDB(span, uri);
      }
    }
  }
}

Now we need to define the CIDB object type and give it AJAX methods--the ability to send requests (using a function defined in drupal.js and to receive and act on responses.

We declare a new object type in Javascript simply by creating a function and setting properties and/or methods. In this case, we'll add a method that calls the drupal.js function HTTPGet().

Like its pair HTTPPost() (used when you want to use a Post rather than a Get operation), HTTPGet() provides a cross-browser method for posting data to the server. It takes three arguments: the uri being requested, the method that should be called when data is returned, and a reference to the calling object (so that the return data can be linked with the correct object instance).

We can use the prototype method to add a new method to the database object--receive, which will handle the data returned by the server.

/**
* A click info DataBase object
*/
function CIDB(elt, uri) {
  var db = this;
  // By making the span element a property of this object,
  // we get the ability to attach behaviours to that element.
  this.elt= elt;
  this.uri = uri;
  this.elt.onclick = function() {
    HTTPGet(db.uri, db.receive, db);
  }
}

/**
* HTTP callback function. Raises an a-lert box
*/
CIDB.prototype.receive = function(string, xmlhttp, cidb) {
  if (xmlhttp.status != 200) {
    return alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ cidb.uri);
  }
  // We have access to the span element, since it's an attribute of the cidb object.
  // Remove the 'clickInfo' class, to show that this is already clicked.
  // We do this with another of the functions in drupal.js
  removeClass(cidb.elt, 'clickInfo');
  alert(string);
}

3. The Handler

To handle the requests sent by AJAX clients, you need at least two pieces of code in your module: (1) a request handler function, and (2) a menu item allowing access to the handler.

The PHP handler is simply a function that receives input and returns data. In some cases you might wish to return XML or some other sort of encoded data to be parsed by your Javascript (see user_autocomplete() for an example), or fully formed HTML elements to be appended to user display.

In our case, all we need is a phrase that's going to be output.

<?php
function click_info_ajax_example_handler() {
 
// We've appended the word in question to the uri as the third argument.
 
$string = arg(2);
  switch (
$string) {
    case
'Drupal':
      print
t('Great software!');
      break;
    default:
      print
t('Nothing');
  }
  exit();
}
?>

In some contexts and depending on the browser being used you might run into caching issues, where a browser won't repeat an AJAX request. If this is an issue, try including additional headers in your PHP responder (before any output) instructing the browser not to cache the data.

<?php
  header
("Cache-Control: no-cache, must-revalidate");
 
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
?>

Now make sure users can reach your handler:

<?php
/**
* Implementation of hook_menu
*/
function click_info_ajax_menu($may_cache) {
 
$items = array();
  if (
$may_cache) {
   
$items[] = array(
     
'path' => 'click_info_ajax/example_handler',
     
'title' => t('click info example'),
     
'access' => user_access('access content'),
     
'type' => MENU_CALLBACK,
     
'callback' => 'click_info_ajax_example_handler'
    
);
   }
  return
$items;
}
?>

Now ensure you send the JS and CSS files as needed (see the section "Send the needed files" in Tutorial 1), and you're away.

See the completed module. To use it:

  • Install and enable the module
  • Create a new node (e.g., a 'story'), and include the word Drupal.
  • View the page. The word Drupal should be highlighted. Click on it to get the alert 'Great software!'.

More examples

Of course, this tutorial has only scratched the surface of what can be done with AJAX. To find out more, study the AJAX widgets that ship with Drupal (e.g., autocomplete.js), or contributed modules using AJAX. Examples include chat_window.js and Javascript Tools modules, including Ajaxsubmit, Dynamicload, and Activemenus.

Happy coding!

$may_cache problems

JamieR - June 24, 2006 - 19:43

I'm not sure why, but the $may_cache variable was comming in unset to the hook_menu in my module. I removed it and was good to go. If I understand it correctly we shouldn't need it for most AJAX applications anyway - but who am I to say, I can barely figure this stuff out! :)

Just thought it might help someone to know!

if (isJsEnabled()) { 

smartango - October 13, 2007 - 17:04

if (isJsEnabled()) {
  addLoadEvent(clickInfoAutoAttach);
}

function clickInfoAutoAttach() {
  var cidb = [];
  var spans = document.getElementsByTagName('span');
  for (var i = 0; span = spans[i]; i++) {
    if (span && hasClass(span, 'clickInfo')) {
      // Read in the path to the PHP handler
      uri = span.getAttribute('title');
      // Remove the title, so no tooltip will display
      span.removeAttribute('title');
      // Create an object with this uri. Because
      // we feed in the span as an argument, we'll be able
      // to attach events to this element.
      if (!cidb[uri]) {
        cidb[uri] = new CIDB(span, uri);
      }
    }
  }
}

in drupal 5.x is better write, using jquery

if (Drupal.jsEnabled) {
  $(document).ready(clickInfoAutoAttach);
}

function clickInfoAutoAttach() {
  var uri= $("span").filter(".clickInfo").attr("title");
  $("span")
    .filter(".clickInfo")
    .click(function(){
  $.get(uri,function(data){alert(data)});
    });
}

___
http://www.smartango.com/

devel module

TapocoL - November 21, 2007 - 18:35

With devel printing info on shutdown (if settings apply), use the following line in your handling function to not run the devel_shutdown printing:

$GLOBALS['devel_shutdown'] = FALSE;

Just a quick note for those that could be affected by this in their AJAX.

-Craig Jackson
-Web Developer

 
 

Drupal is a registered trademark of Dries Buytaert.