Skip to content

Instantly share code, notes, and snippets.

@pgraham
Created December 6, 2012 18:53
Show Gist options
  • Save pgraham/4227071 to your computer and use it in GitHub Desktop.
Save pgraham/4227071 to your computer and use it in GitHub Desktop.
Make any object observable with subtyped events
test( "test interface", function () {
var eventRegistry = window.eventRegistry();
ok(eventRegistry.hasOwnProperty('on'), "No `on` method found");
ok(eventRegistry.hasOwnProperty('trigger'), "No `trigger` method found");
});
test( "test binding", function () {
var eventRegistry = window.eventRegistry();
expect( 1 );
eventRegistry.on('event', function () {
ok( true, "This needs to be called");
});
eventRegistry.trigger('event');
});
test( "sub event binding", function () {
var eventRegistry = window.eventRegistry();
expect( 3 );
eventRegistry.on('event', function () {
ok( true, "This should be called");
});
eventRegistry.on('event.triggered', function () {
ok( true, "This should be called");
});
eventRegistry.on('event.triggered.triggered', function () {
ok( true, "This should be called");
});
eventRegistry.on('event.triggered.notTriggered', function () {
ok( false, "This should not be called");
});
eventRegistry.on('event.notTriggered', function () {
ok( false, "This should not be called");
});
eventRegistry.trigger('event.triggered.triggered');
});
test( "sub event invocation order", function () {
var eventRegistry = window.eventRegistry();
var level1 = false, level2 = false, level3 = false;
eventRegistry.on('leastSpecific', function () {
ok(!level1, "Level 1 handler should not have been invoked");
ok(level2, "Level 2 handler should have been invoked");
ok(level3, "Level 3 handler should have been invoked");
level1 = true;
});
eventRegistry.on('leastSpecific.moreSpecific', function () {
ok(!level1, "Level 1 handler should not have been invoked");
ok(!level2, "Level 2 handler should not have been invoked");
ok(level3, "Level 3 handler should have been invoked");
level2 = true;
});
eventRegistry.on('leastSpecific.moreSpecific.mostSpecific', function () {
ok(!level1, "Level 1 handler should not have been invoked");
ok(!level2, "Level 2 handler should not have been invoked");
ok(!level3, "Level 3 handler should not have been invoked");
level3 = true;
});
eventRegistry.trigger('leastSpecific.moreSpecific.mostSpecific');
ok( level1 && level2 && level3 );
});
test( "event data: one parameter", function () {
var eventRegistry = window.eventRegistry();
var evData = { value: 'some data' };
expect( 2 );
eventRegistry.on('a', function (ev, data) {
ok(ev.type === 'a', "Event object does not contain proper type");
ok(data === evData, "Data is not the right object");
});
eventRegistry.trigger('a', [ evData ]);
});
test( "event data: multiple parameters", function () {
var eventRegistry = window.eventRegistry();
var evData1 = { value: 'some data' }, evData2 = { value: 'some more data' };
expect( 3 );
eventRegistry.on('a', function (ev, data1, data2) {
ok(ev.type === 'a', "Event object does not contain proper type");
ok(data1 === evData1, "`data1` is not the right object");
ok(data2 === evData2, "`data2` is not the right object");
});
eventRegistry.trigger('a', [ evData1, evData2 ]);
});
test( "observable.one", function () {
var eventRegistry = window.eventRegistry();
var evData = { value: 'some data' };
var invoked = false;
expect( 2 );
eventRegistry.one('a', function (ev, data) {
ok( data === evData, "`data` is not passed" );
ok( !invoked, "This should only be called once" );
invoked = true;
});
eventRegistry.trigger('a', [ evData ]);
eventRegistry.trigger('a', [ evData ]);
});
test( "no handlers", function () {
var eventRegistry = window.eventRegistry();
eventRegistry.trigger('event');
ok( true, "As long as there is no error everything is good" );
});
test( "no subtype handlers", function () {
var eventRegistry = window.eventRegistry();
eventRegistry.on('event', function () {
ok( true, "This should be called but that's not what is being tested" );
});
eventRegistry.trigger('event.subtype');
ok( true, "As long as there is no error everything is good" );
});
/**
* Make object observable with subtyped events.
*
* Event hierarchy is specified using dots (.)
*
* observable({}).on('type.child.leaf', function () {});
*
* When an event of type `type.child.leaf` is triggered, then handlers for
* events of type `type` and `type.child` will also be triggered. Event
* handlers registered for a deeper event type are considered to be "more
* specific" since they will be triggered by fewer event types. When an event
* with a substype is triggered, more specific handlers will be invoked first.
*/
(function (exports, undefined) {
"use strict";
function eventRegistry() {
var handlers = [], registry = {}, that = {};
function removeHandler(handler) {
var i, len;
for (i = 0, len = handlers.length; i < len; i++) {
if (handlers[i] === handler) {
handlers.splice(i, 1);
break;
}
}
}
that.addHandler = function (handler, subtypes) {
if (subtypes.length === 0) {
handlers.push(handler);
return;
}
var basetype = subtypes.shift();
if (!registry.hasOwnProperty(basetype)) {
registry[basetype] = eventRegistry();
}
registry[basetype].addHandler(handler, subtypes);
};
that.getHandlers = function (subtypes) {
if (subtypes.length === 0) {
return [].concat(handlers);
}
var basetype = subtypes.shift();
if (!registry.hasOwnProperty(basetype)) {
return [].concat(handlers);
}
return registry[basetype].getHandlers(subtypes).concat(handlers);
};
that.removeHandler = function (handler, subtypes) {
if (subtypes.length === 0) {
removeHandler(handler);
return;
}
registry[subtypes.shift()].removeHandler(handler, subtypes);
};
return that;
}
exports.observable = function (that) {
var registry = eventRegistry();
that.trigger = function (event, data) {
var handlers = registry.getHandlers(event.split('.')), i, len;
for (i = 0, len = handlers.length; i < len; i++) {
handlers[i].apply(this, [ { type: event } ].concat(data));
}
return this;
};
that.on = function (type, handler) {
var events = type.split(' '), i, len;
for (i = 0, len = events.length; i < len; i++) {
registry.addHandler(handler, events[i].split('.'));
}
return this;
};
that.one = function (type, handler) {
var wrapper = function () {
handler.apply(this, arguments);
that.off(type, wrapper);
};
that.on(type, wrapper);
};
that.off = function (type, handler) {
var events = type.split(' '), i, len;
for (i = 0, len = events.length; i < len; i++) {
registry.removeHandler(handler, events[i].split('.'));
}
return this;
};
return that;
};
/**
* Convenience function for creating an empty observable object that can be
* used as an EventBus.
*/
exports.eventRegistry = function () {
return observable({});
};
} (window));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment