Update, 25 February 2008: This class is now available as part of
JS.Class (it’s called MethodChain
now). It also forms a key part of
Ojay, an expressive wrapper for YUI.
Update, 12 Dec 2007: Another implementation change. A blank
ChainCollector
instance now has the following properties: then
, and
,
____
(formerly __enqueue
) and fire
. The method queue and base object are
private variables, and addMethods
is a static method. I’m trying to make it as
flexible as possible, i.e. it should have the fewest possible named properties
but still allow you to extend it to your needs. Also, you now add methods to
ChainCollector
’s prototype rather than to instances as this is much faster.
Update, 9 Dec 2007: I’ve modified the implementation of ChainCollector
so it has fewer methods in its prototype, thus fewer chances of name collisions.
It no longer has initialize
, __addMethods
or __enqueue
methods – these
are now ‘private’ methods created using the module pattern.
For this post, I’m going to be using jQuery, because it’s the closest well-known thing to the library I’m currently working on at my new job. (It’s a wrapper for YUI with lots of syntax niceties.)
Programming asynchronous actions is a pain in the head. Why can’t I do
setTimeout($('#myNode').hide, 2000)
? I need to bind the hide
function to the
$('#myNode')
object for starters, and jQuery doesn’t give you a bind
method,
and besides, binding and execution scope gives lots of JavaScript novices a
headache. Wouldn’t this be nice:
$('#myNode').wait(2).then.hide();
Over the weekend, I was trying to figure out a general purpose way of adding asynchronous behaviour like this, so I could use it with event handlers, Ajax calls, post-animation callbacks etc. My inspiration came from Methodphitamine (site unavailable, try the Google cache).
I won’t bore any non-Rubyists with the details, but the idea is quite simple:
create an object with no predefined methods, which accepts any method call and
adds the name of the method and its arguments to a queue. This queue can be
turned into a function and called on whatever object you want at a later time.
Unfortunately, JavaScript has no analogue for Ruby’s method_missing
, which
means if you want such an object in JavaScript, you need to predefine every
method name you might want to use. Big pain.
But, you can get something quite usable if you’re implementing an interface (like jQuery’s) that allows chaining. You can pass some object to the constructor for your magical queue-collecting object, and have it implement copies of all the object’s methods. If you need methods from other objects, you can pass those in too.
An example, implementing my wait
suggestion from above:
jQuery.fn.wait = function(time) {
var collector = new ChainCollector(), self = this;
// Deal with scoping issues...
var fire = function() { collector.fire(self); };
setTimeout(fire, Number(time) * 1000);
return collector;
};
// Then extend ChainCollector with all jQuery's methods
ChainCollector.addMethods(jQuery);
With this defined (and ChainCollector
, code to follow), you can do this:
$('#jq-header').wait(2).then.hide('slow')
Grab all the code from this post and go try that out on the jQuery home
page. Nice, no? I’m using this technique for writing concise, extensible
event handlers at the moment, and it’s very nice indeed. You can even chain
multiple wait
calls into the same statement:
$('#jq-header').wait(2).then.hide('slow').wait(3).then.show('fast')
Anyway, now that I’ve got you all fired up about this, here’s the magic that makes it all possible:
var ChainCollector = function(base) {
var CLASS = arguments.callee;
this.then = this.and = this;
var queue = [], baseObject = base || {};
this.____ = function(method, args) {
queue.push({func: method, args: args});
};
this.fire = function(base) {
var object = base || baseObject, method, property;
for (var i = 0, n = queue.length; i < n; i++) {
method = queue[i];
if (object instanceof CLASS) {
object.____(method.func, method.args);
continue;
}
property = object[method.func];
object = (typeof property == 'function')
? property.apply(object, method.args)
: property;
}
return object;
};
};
ChainCollector.addMethods = function(object) {
var methods = [], property, i, n, name;
var self = this.prototype;
var reservedNames = [], blank = new this();
for (property in blank) reservedNames.push(property);
var re = new RegExp('^(?:' + reservedNames.join('|') + ')$');
for (property in object) {
if (Number(property) != property)
methods.push(property);
}
if (object instanceof Array) {
for (i = 0, n = object.length; i < n; i++) {
if (typeof object[i] == 'string')
methods.push(object[i]);
}
}
for (i = 0, n = methods.length ; i < n; i++)
(function(name) {
if (re.test(name)) return;
self[name] = function() {
this.____(name, arguments);
return this;
};
})(methods[i]);
if (object.prototype)
this.addMethods(object.prototype);
};
I’d love to know what uses people find for this.