New possibilities with modules in JS.Class 2.0

It’s been out and about a couple months now, and I’ve been putting it to good use in the upcoming release of Ojay. The new version (fingers crossed it’ll be out by the end of the month) features an extension to the custom event system that lets events published using Observable ‘bubble’ up the type system. This means that you can, for example, register an event listener on the Ojay.Paginator class and get notified whenever an instance of that class fires the required event, making it really easy to add behaviour to classes without modifying their source code or creating entirely new subclasses.

Here I’m going to dig in and show how this works in JS.Class 2.0 using a little reflection to get the job done. At time of writing, this is Ojay’s custom event library in its entirity:

Ojay.Observable = new JS.Module({
    include: JS.Observable,
    
    on: function(eventName, callback, scope) {
        var chain = new JS.MethodChain;
        if (callback && typeof callback != 'function')
            scope = callback;
        this.addObserver(function() {
            var args = Array.from(arguments),
                message = args.shift();
            if (message != eventName) return;
            if (typeof callback == 'function')
                callback.apply(scope || null, args);
            chain.fire(scope || args[0]);
        }, this);
        return chain;
    },
    
    notifyObservers: function() {
        var args = Array.from(arguments),
            receiver = (args[1]||{}).receiver || this;
        
        if (receiver == this) args.splice(1, 0, receiver);
        else args[1] = receiver;
        
        this.callSuper.apply(this, args);
        
        args[1] = {receiver: receiver};
        var classes = this.klass.ancestors(), klass;
        while (klass = classes.pop())
            klass.notifyObservers &&
                    klass.notifyObservers.apply(klass, args);
        
        return this;
    },
    
    extend: {
        included: function(base) {
            base.extend(this);
        }
    }
});

Ojay.Observable.extend(Ojay.Observable);

As you can see, this is a wrapper around JS.Observable, which itself is a pretty small chunk of code. This module implements two methods: Ojay.Observable#on() and Ojay.Observable#notifyObservers(). The former is used by clients to register event listeners and supports some magic in the form of MethodChain, and the latter is a wrapper around JS.Observable#notifyObservers() that implements the bubbling effect.

You needn’t concern yourself with the details of the on() method, or half of notifyObservers() for that matter. The starting point for the bubbling effect is this included() hook:

    extend: {
        included: function(base) {
            base.extend(this);
        }
    }

This means that, whenever a class mixes in Ojay.Observable using include, the methods of Ojay.Observable are also added to the class as singleton methods; so the class and all its instances are all observable objects. We can attach event listeners to the class itself, not just to its instances.

Event dispatch is implemented by the following lines in notifyObservers():

        this.callSuper.apply(this, args);
        
        var classes = this.klass.ancestors(), klass;
        while (klass = classes.pop())
            klass.notifyObservers &&
                    klass.notifyObservers.apply(klass, args);

callSuper is used to let JS.Observable dispatch the event on the firing object, then we use JS.Module#ancestors() to look up all the classes and modules in the firing object’s inheritance tree. We then loop over them, and if any of them are observable we call their notifyObservers() method to propagate the event. This is really simple but it lets you do some powerful things with the type system and makes extending existing classes very easy indeed.

(The eagle-eyed among you will notice that this might cause an infinite recursion, since each ancestor class will look up its ancestors classes when we call notifyObservers on it. However, for classes and modules (the ancestors of the source object), this.klass.ancestors() will return a list containing at most the JS.Class, JS.Module and JS.ObjectMethods modules, none of which are observable.)

The final little trick is this line:

Ojay.Observable.extend(Ojay.Observable);

This adds the methods of Ojay.Observable as singleton methods to Ojay.Observable; since this module will be in the inheritance tree of any object firing events, it makes sense to let us register event listeners on this module to catch any event fired anywhere in the codebase. For example, I can write:

Ojay.Observable.on('show', function(object) {
    // ... do something with object
});

and my callback will be notified whenever any object fires a show event. object will be a reference to the object firing the event, so you can access objects in memory without having explicit references to them beforehand.

To my mind, this method of dispatching events ties in very nicely with the DOM event model, especially if you’re using CSS selectors to choose nodes to listen to. In the DOM, you’re typically choosing some type of object to listen to; you register a listener on all links with a class of "overlay", for example. This takes that concept and maps it to JavaScript objects, allowing you to listen to whole classes of objects with a single listener.