Using ChainCollector to respond to Ajax calls

Saq made a couple of comments on my ChainCollector article about how to queue up functions to respond to Ajax calls, and whether I could write something up to shed a bit of light on how this might be done. Today, I’m going to implement some methods that allow to GET from/POST to a URL, then do some basic things with the response using very sentence-like code. Specifically, I’m going to end up with:

Ajax.GET('/api').then.insertInto('#blog-excerpt').and.evalScripts()

(I hope you’re starting to see why I wrote ChainCollector: code clarity is something that’s very important to me, especially when you’re working in a team that covers the whole web software stack and many of them don’t know JavaScript.) I’m not sure if this exactly answers Saq’s problem, but I hope it will illustrate how you might begin using ChainCollector to solve issues like this.

Okay, the first thing we’re going to need is a new class that provides us with methods to use in the chain above. Let’s call it ChainableRequest. I’m using Prototype today, but hopefully non-Prototype users will be able to follow along.

Ajax.ChainableRequest = Class.create();

Object.extend(Ajax.ChainableRequest.prototype, {
  initialize: function(verb, url) {
    this.chain = new ChainCollector(this);
    this.request = new Ajax.Request(url, {
      method: verb,
      onComplete: function(transport) {
        this.response = transport;
        this.chain.fire(this);
      }.bind(this)
    });
  }
});

This class takes two arguments to initialize its instances: an HTTP verb (GET or POST) and a URL. It sets up a new ChainCollector (which will inherit any methods we add to ChainableRequest) and sets off an Ajax request to the URL. It registers a callback that tells the request to fire the chain when the request completes.

So, onto the methods that we want to add to the chain. We need a method that inserts the response into some elements on the page. I want this method to accept a CSS selector, an element reference, or an array of element references. For each element found, it strips any scripts out of the response and inserts the remainder into the element. Note how each method returns the ChainableRequest object for chaining purposes.

Object.extend(Ajax.ChainableRequest.prototype, {
  insertInto: function(elements) {
    if (!this.response) return this;
    if (typeof elements == 'string') elements = $$(elements);
    if (!(elements instanceof Array)) elements = [elements];
    elements.each(function(element) {
      element.innerHTML = this.response.responseText.stripScripts();
    }.bind(this));
    return this;
  }
});

And, we need a method to evaluate the scripts in the response. You’ll see that Prototype uses a setTimeout in some cases to do this, in case the document hasn’t finished updating the DOM in response to an innerHTML change.

Object.extend(Ajax.ChainableRequest.prototype, {
  evalScripts: function() {
    if (!this.response) return this;
    setTimeout(function() {
      this.response.responseText.evalScripts();
    }.bind(this), 10);
    return this;
  }
});

The final piece of the puzzle is to create methods for any HTTP verbs you want to use. Note that I’m using capitals because that’s the convention with HTTP verbs, and also because you might want to go on an implement PUT and DELETE (supported by YUI), and delete is a reserved word in JavaScript. Each verb method should create a new ChainableRequest, then return its chain property so you can chain methods into the onComplete callback.

$w('GET POST').each(function(verb) {
  Ajax[verb] = function(url) {
    var req = new Ajax.ChainableRequest(verb, url);
    return req.chain;
  };
});

// Remember to add the required methods to ChainCollector
ChainCollector.addMethods(Ajax.ChainableRequest);

And that just about wraps it up in terms of getting our initial code sentence working. You’d probably want something a lot more flexible than this in real life, but this covers some common uses for Ajax calls that can easily be turned into sentence structures. You can download the ChainableRequest JavaScript class to save yourself copy-pasting all the code from this article.