Now comes the fun part - listening for events. There are many special needs that arise when listening for and responding to events, so this will be more involved than the source-side of the discussion. Let's start as simply as possible.
var obj = new Base();
obj.on("foo", this.onFoo, this);
// The following will get an exception (obj has no "bar" event):
obj.on("bar", ...); // throws
In this simple example, some instance method of another object wants to listen to foo events from a Base object. The arguments are the event name ("foo"), the method to call (this.onFoo) and the instance scope to use when calling the method (this).
If you are familiar with ExtJS, you'll notice that this is pretty familiar and that is deliberate. I think ExtJS has a very clean model for event connection, so I followed its pattern. This continues for multiple event connections:
obj.on(
{
foo : this.onFoo,
bar : this.onBar,
scope: this // applied to onFoo and onBar
});
// Or:
obj.on(
{
foo : this.onFoo,
bar : { fn : that.onBar, scope: that },
scope: this // applies only to onFoo
});
And there is also the ability to filter out bubbled events (more on those next time).
obj.on(
{
foo : this.onFoo,
scope: this,
target: obj
});
And lastly, there is the ability to be fired only a certain number of times (often 1 for "just once"). This is a bit different than ExtJS.
obj.on(
{
foo : this.onFoo,
scope: this,
only: 1
});
// calls onFoo only 1 time (then disconnects)
// or
obj.on(
{
foo : this.onFoo,
bar : { fn : that.onBar, scope: that, only: 2 },
scope: this // only applies to this.onFoo
});
// calls onBar twice, but onFoo remains indefinitely
If you are really familiar with ExtJS, you're probably thinking I've missed a few things here. You are right if you noticed that "delay" and "buffer" are not present. I opted to implement these features in a more general way.
Not Now!
It is often the case that an event needs to be handled "really soon" after it is detected, but not immediately. Perhaps some of the state hasn't settled yet and you need to let the callstack unwind all the way out before doing the processing for the event. This kind of delaying is not limited to event handling, so I implemented that feature as a Function.prototype extension.
obj.on("foo", this.onFoo.delayed(10), this);
Everytime "foo" fires, the onFoo method is called 10msec later. The more complex buffer approach is also implemented as a Function.prototype extension, and has more flexibility as well.
// 10msec delay after LAST firing of foo
obj.on("foo", this.onFoo.buffered(), this);
// call 25msec after the LAST firing of foo:
obj.on("foo", this.onFoo.buffered(25), this);
// call 25msec after the FIRST firing of foo:
obj.on("foo", this.onFoo.buffered(-25), this);
// call 10msec after the LAST firing of foo but with
// the args from the FIRST:
obj.on("foo",
this.onFoo.buffered(
{
args: "first",
delay: 10
}),
this);
As you can see, the buffered method has a few more bells and whistles. Its 2nd argument is an optional "scope" parameter that determines the "this" context used for the method call. Since this is also handled by the eventing mechanism, it is not needed in this case but might be useful elsewhere.
Everyone Out Of The Pool!
It's all too easy. Fun and games really. Buy what happens when want to stop listening? Consider this ExtJS scenario:
var obj = new Base();
obj.on("foo", function () { ... }, this);
This works just fine - unless you need to disconnect such a thing. Which you cannot do as coded. This is because to disconnect a listener in ExtJS (and most other systems, e.g. our beloved DOM), you have to supply the same arguments passed at the time of connection. This can be very restrictive because you cannot use function literals or any form of function producer (like bind, head, tail, etc.).
I decided that the way window.setTimeout works is much better: it produces a token which is later used to deconstruct things via window.clearTimeout. In this case, however, the object returned by on has a destroy method which enables us to use the handy zjs.destroy function.
var obj = new Base();
this.unfoo = obj.on("foo", this.onFoo, this);
// ... later ...
zjs.destroy(this.unfoo);
A simple way to handle multiple such connections would be:
var obj = new Base();
this.unall = [ obj.on("foo", this.onFoo, this),
obj2.on("bar", ... ),
obj3.on("bif", ... ),
obj4.on("jax", ... ),
obj5.on("raz", ... ) ]
// ... later ...
zjs.destroy(this.unall); // destroys each array element
While this is a departure from most other event subscription approaches, it really simplifies the process by which cleanup is accomplished. And just in case you were curious: if the on method connected multiple event listeners, the returned object's destroy method would, of course, disconnect all of them.
Miscellaneous
Wrapping up, there is a helper method behind the delayed method that can be useful:
this.method.later(10, this);
This is very similar to the defer method provided by ExtJS, but it uses the destroyable object return value approach for consistency:
var t = this.method.later(10, this);
zjs.destroy(t); // never mind