AjaxEvent: Using YUI custom events with Ajax

Published 16:58 on 13 July, 2007

Recently, I’ve been working on some fairly Ajax intensive pages and, because I’m working in a team environment, the need arose for a simple bit of script to handle all the different Ajax “events”. This script needed to be small yet easy to implement

Enter the YUI. cue Enter The Dragon theme

So here’s a tasty little script that wraps Ajax requests and a YUI Custom Event in an instantiable object that works as an Ajax DAO

The Concept

Ajax

Ajax is a buzz-word. It originally stood for “Asynchronous JavaScript And XML”, however, these days it tends to encompass any form of remote-scripting; whether it returns XML or not.

Personally, I’m not a big fan of the term, preferring to call it “remote scripting” like I always have – however, sometimes buzz-words can work to your advantage when you’re trying to sell something to the money men, your bosses, or, indeed, the search engines.

If you want to read up on Ajax, I’d recommend starting with Jesse James Garrett’s seminal article, “Ajax: a new approach to Web Applications”; it’s a good introduction to the concepts.

Following that, I’d recommend dipping into the MDC Ajax documentation as it forms a sound base to start from.

Finally, it’s worth pointing out that any good JavaScript library will contain some form of Ajax handler to make all that cross-browser nonsense a little easier to chew. In this script, I’ll be utilising the excellent YUI Connection Manager.

Custom Events

The interaction between the user interface and JavaScript is “event” based. Every time the user does something on the page, an event is fired. JavaScript allows us add function calls to each of these events, so that we can trigger some form of functionality.

Good examples of events would be “onmouseover”, “onclick”, and “onload” (although the latter is one of the few events that is not triggered by the user – rather, it is triggered as the page finishes loading).

For more information about JavaScript events, take a look at PPK’s excellent “Introduction to Events” article.

“Custom” events are, in effect, an extension of this concept; except they’re defined by you, as an interface developer. Good examples of custom events might be “ontabchange” on a tab module, “onmenushow” or “onmenuhide” on a drop-down menu, or even stuff like “ondrag” when implementing drag/drop functionality.

In theory, you don’t even need any special code to do this; just some form of method call when the event happens – although, when you think about it, it would be much nicer to implement the Observer design pattern and allow another developer to “subscribe” to your event.

It’s exactly for this reason that the YUI Event Utility allows the creation of custom events, and it’s that functionality that I’m going to use.

For more information on custom events, I’d recommend having a look at Christian Heilmann’s excellent YUI Blog post, “Event-Driven Web Application Design”. It’s also worth having a look at “Publishing Custom Events in JavaScript” by Dustin Diaz.

Mixing it all up

YUI Connection Manager handles Ajax connections very well – but imagine we have a page containing a number of Ajax dependant widgets or UI components that ALL need to fire off their own asynchronous requests. Add to this the fact that we have a number of methods that may need to be run with the data returned from our request, and you have the potential for a lot of mutually exclusive connections that don’t necessarily need to be exclusive.

In this particular case, it would be better to create an object to handle all our Ajax requests; in much the same way that a dedicated DAO would handle database requests in any other object-oriented system. This object could then be used to manage requests and the methods to fire when those requests are successful.

To best achieve this, we wrap our YUI Connection object and our YUI Custom Event inside another instantiable (is that even the right word?) object – AjaxEvent.

The Method

The AjaxEvent Object

First things first, it’s important to make sure our code is namespaced. This basically stops us overwriting other code elsewhere, and encapsulates all our methods nicely.

For more about namespacing JavaScript, take a look at Jon Snook’s short post, “JavaScript Namespaces”.

So basically, we need to check to see if a namespace object exists. If it does, we add our methods to that object, if not, we create a new object to add our methods to.

We do that like so:

var NEF = window.NEF || {};

Next we define our object in a way that allows us to instantiate it using the new operator. We do this simply so that we can have multiple AjaxEvents on our page that maintain their properties:

NEF.AjaxEvent = function() {
  return this;
}

Note that our object contains no members and simply returns itself on construction. This means that it will instantiate quickly and won’t slow down our scripts. We can use prototypal inheritance to add methods later.

Now let’s take a look at the methods we’re going to add. Firstly, there’s our subscribe method, which will allow us to subscribe methods to our object’s custom event:

  /*
   * Subscribes a function or method to the custom event.
   *
   * @method subscribe
   * @param {Function}  fn          The function to execute
   * @param {Object}    oScope      An object to be passed as scope for the function
   */
  subscribe: function(fn, oScope) {
    if (!this.oEvent) {
      // Check to see if this.oEvent exists and if not, create it.
      this.oEvent = new YAHOO.util.CustomEvent("ajaxevent", this, false, YAHOO.util.CustomEvent.FLAT);
    }
    if (oScope) {
      // If oScope exists, we subscribe fn to this.oEvent, passing
      // oScope as it's scope object.
      this.oEvent.subscribe(fn, oScope, true);
    } else {
      // Otherwise, we simply subscribe fn to this.oEvent.
      this.oEvent.subscribe(fn);
    }
  }

This is all fairly straight forward and simply creates our custom event (if it hasn’t been created already) as a member of our AjaxEvent. It then subscribes the function fn to it, using a scope object if one is passed.

Next we need to specify a method to perform our Ajax request:

  /*
   * Performs an asynchronous request using the YUI Connection manager
   * and fires our custom event upon success.
   *
   * @method connect
   * @param {String}    sUri        Fully qualified path of resource
   */
  connect: function(sUri) {
    if (!sUri && !this.sUri) {
      // If the sUri parameter has not been passed and the object's
      // sUri member has not been set then return false as we have
      // no where to make our Ajax call to.
      return false;
    } else {
      // Set up the object's sUri member to the most recent
      // location that's been passed.
      this.sUri = (!sUri) ? this.sUri : sUri;

      // We need to prevent Internet Explorer from caching our
      // Ajax request. To do this, we'll add the current date/time as
      // a unix timestamp onto the URI -- which will always be unique.
      // Firstly we obtain the current date/time as a unix timestamp
      this.dt = new Date().valueOf();

      // Next we work out how's best to add the number and add it.
      this.sUri = (this.sUri.indexOf("?") === -1) ? this.sUri + "?yaetime=" + this.dt : ((this.sUri.indexOf("yaetime") === -1) ? this.sUri + "&yaetime=" + this.dt : this.sUri.replace(/yaetime=[0-9]+/i, "yaetime=" + this.dt));

      // Finally we fire off our asynchronous request using the YUI
      // connection manager.
      YAHOO.util.Connect.asyncRequest('GET', this.sUri, {
        success: function (o) {

          // Here we're making the assumption that our response
          // is JSON. We eval it...
          var oJSON = eval("(" + o.responseText + ")");

          // ... and then pass it to our subscribed functions
          // as a parameter (as long as it's valid).
          this.oEvent.fire((typeof oJSON === "object") ? oJSON : null);
        },
        scope: this
      });
    }
  }

This method will ultimately return our JSON object as a parameter of the functions subscribed to our event. This means that we have instant access to any members of said object within that function.

Implementation

We implement our object like so:

// To begin with, we instantiate our object.
var myAjaxEvent = new NEF.AjaxEvent();

// Next we subscribe a function to our object. I'm doing this
// anonymously here (as a lambda function), but there's no reason
// we couldn't pass it a function that had already been defined.
myAjaxEvent.subscribe(function (o) {
	// The parameter o is our returned JSON object on success of the Ajax.
	// As such, we can access its members just like any other object.
  alert(o.property);
});

// Finally, to fire off the Ajax request, we simply pass a fully qualified URI
// to the connect method...
myAjaxEvent.connect('gimmesomejson.php?skeleton=24');

// ...and should we wish to fetch that result again, we can call connect again
// without a parameter.
myAjaxEvent.connect();

// Should we want to change the URI, for whatever reason (eg. new HTTP GET), we
// can by simply passing a new URI.
myAjaxEvent.connect('gimmesomejson.php?skeleton=3');

So there you have it; a small script that manages and simplifies multiple Ajax calls. It’s not groundbreaking, but it’s certainly made life easier in our multi-developer environment.

The Complete Script

Finally, here’s the complete script (with the incidental comments removed):

var NEF = window.NEF || {};

/*
 * The AjaxEvent class allows you to create DAOs for handling
 * Ajax requests. Upon request success, it fires a custom event
 * that handles subscribed functions.
 *
 * @namespace NEF
 * @class AjaxEvent
 * @constructor
 */
NEF.AjaxEvent = function() {
  return this;
}

NEF.AjaxEvent.prototype = {
  
  /*
   * Subscribes a function or method to the custom event.
   *
   * @method subscribe
   * @param {Function}  fn          The function to execute
   * @param {Object}    oScope      An object to be passed as scope for the function
   */
  subscribe: function(fn, oScope) {
    if (!this.oEvent) {
      this.oEvent = new YAHOO.util.CustomEvent("ajaxevent", this, false, YAHOO.util.CustomEvent.FLAT);
    }
    if (oScope) {
      this.oEvent.subscribe(fn, oScope, true);
    } else {
      this.oEvent.subscribe(fn);
    }
  },
  
  /*
   * Performs an asynchronous request using the YUI Connection manager
   * and fires our custom event upon success.
   *
   * @method connect
   * @param {String}    sUri        Fully qualified path of resource
   */
  connect: function(sUri) {
    if (!sUri && !this.sUri) {
      return false;
    } else {
      this.sUri = (!sUri) ? this.sUri : sUri;
      this.dt = new Date().valueOf();
      this.sUri = (this.sUri.indexOf("?") === -1) ? this.sUri + "?yaetime=" + this.dt : ((this.sUri.indexOf("yaetime") === -1) ? this.sUri + "&yaetime=" + this.dt : this.sUri.replace(/yaetime=[0-9]+/i, "yaetime=" + this.dt));
      YAHOO.util.Connect.asyncRequest('GET', this.sUri, {
        success: function (o) {
          var oJSON = eval("(" + o.responseText + ")");
          this.oEvent.fire((typeof oJSON === "object") ? oJSON : null);
        },
        scope: this
      });
    }
  }
}

/*
 * Example of Use
 */

var myAjaxEvent = new NEF.AjaxEvent();

myAjaxEvent.subscribe(function (o) {
  alert(o.property);
});

myAjaxEvent.connect('gimmesomejson.php?skeleton=24');
myAjaxEvent.connect();

myAjaxEvent.connect('gimmesomejson.php?skeleton=3');

Demo

Here’s a nice demo of AjaxEvent in action

Improvements

The Ajax connection within my script is somewhat limiting; there’s really no reason why we couldn’t allow developers to specify an HTTP method, and to allow for POST variables.

I’m also assuming that the request returns JSON formatted data which, obviously, may not be the case in other environments. Once again, there’s no reason why we can’t allow the returned data to be manipulated outside the script; thus opening up any number of different opportunities.

Ultimately, in developing this script, we were simply trying to solve our own problems and didn’t really think about developing it further. Please feel free if you choose to do so – but please, let me know because I’d be interested in seeing the results!

Credit where it's due

I really can’t claim sole credit for this script – the original idea was the brainchild of Yahoo! Europe JavaScript luminary Mark Aidan Thomas (who sadly shies away from maintaining any kind of online presence) – I just coded it, and then settled into script tennis; with a number of iterations going back and forth between us.

I’d also like to thank Lawrence Carvalho for being a great sounding board, a wealth of knowledge on the YUI, and generally sticking his oar in where appropriate.

Summary

Hopefully this script will prove useful to someone else; after all, that is why I’m blogging it.

Once again, this script is only really useful if you page is absolutely loaded with Ajax requests. The page that facilitated the development of this script incorporated 7 different Ajax calls, each doing something slightly different and some requests repeating every 30 seconds. As you can well imagine, this would have been a coding nightmare without this script – even more so considering each Ajax call was being written by a different developer.

In the past I’ve been more comfortable using Prototype and Scriptaculous as my preferred JavaScript libraries. Unsurprisingly, since I moved to Yahoo!, the YUI is now my weapon of choice – and to be honest, I’m really beginning to appreciate it. Without the functionality of the CustomEvent object and the Connection manager, the script above would have been a lot more complicated.