Skip to content

Commit

Permalink
Import Event Dispatcher and tests. r=gmarty (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
azasypkin committed Apr 22, 2016
1 parent 7cbedf2 commit ee6997d
Show file tree
Hide file tree
Showing 2 changed files with 515 additions and 67 deletions.
107 changes: 40 additions & 67 deletions app/js/lib/foxbox/event-dispatcher.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,26 @@
/*
* This file provides an helper to add custom events to any object.
*
* In order to use this functionality with any object consumer can either
* inherit target object class from EventDispatcher or mix necessary methods
* into object directly using the 'EventDispatcher.mixin' static method:
* In order to use this functionality with any object consumer should extend
* target object class with EventDispatcher:
*
* class Obj extends EventDispatcher {}
* const obj = new Obj();
*
* or
*
* const obj = EventDispatcher.mixin(new SomeObj());
*
* A list of events can be optionally provided and it is recommended to do so.
* If a list is provided then only the events present in the list will be
* allowed. Using events not present in the list will cause other functions to
* throw an error:
*
* class Obj extends EventDispatcher {
* constructor() {
* super([
* 'somethinghappened',
* 'somethingelsehappened'
* ]);
* super(['somethinghappened', 'somethingelsehappened']);
* }
* }
* const obj = new Obj();
*
* or
*
* const obj = EventDispatcher.mixin(new SomeObj(), [
* 'somethinghappened',
* 'somethingelsehappened'
* ]);
*
* The wrapped object will have five new methods: 'on', 'once', 'off', 'offAll'
* and 'emit'. Use 'on' to register a new event-handler:
* The object will have five new methods: 'on', 'once', 'off', 'offAll' and
* 'emit'. Use 'on' to register a new event-handler:
*
* obj.on("somethinghappened", function onSomethingHappened() { ... });
*
Expand Down Expand Up @@ -73,19 +58,19 @@
* obj.emit("somethinghappened", 123);
*/

function ensureValidEventName(eventName) {
function assertValidEventName(eventName) {
if (!eventName || typeof eventName !== 'string') {
throw new Error('Event name should be a valid non-empty string!');
}
}

function ensureValidHandler(handler) {
function assertValidHandler(handler) {
if (typeof handler !== 'function') {
throw new Error('Handler should be a function!');
}
}

function ensureAllowedEventName(allowedEvents, eventName) {
function assertAllowedEventName(allowedEvents, eventName) {
if (allowedEvents && allowedEvents.indexOf(eventName) < 0) {
throw new Error('Event "' + eventName + '" is not allowed!');
}
Expand All @@ -108,13 +93,14 @@ export default class EventDispatcher {

/**
* Registers listener function to be executed once event occurs.
*
* @param {string} eventName Name of the event to listen for.
* @param {function} handler Handler to be executed once event occurs.
*/
on(eventName, handler) {
ensureValidEventName(eventName);
ensureAllowedEventName(this[p.allowedEvents], eventName);
ensureValidHandler(handler);
assertValidEventName(eventName);
assertAllowedEventName(this[p.allowedEvents], eventName);
assertValidHandler(handler);

let handlers = this[p.listeners].get(eventName);
if (!handlers) {
Expand All @@ -129,31 +115,33 @@ export default class EventDispatcher {
/**
* Registers listener function to be executed only first time when event
* occurs.
*
* @param {string} eventName Name of the event to listen for.
* @param {function} handler Handler to be executed once event occurs.
*/
once(eventName, handler) {
ensureValidHandler(handler);
assertValidHandler(handler);

const once = (parameters) => {
this.off(eventName, once);

handler(parameters);
handler.call(this, parameters);
};

this.on(eventName, once);
}

/**
* Removes registered listener for the specified event.
*
* @param {string} eventName Name of the event to remove listener for.
* @param {function} handler Handler to remove, so it won't be executed
* next time event occurs.
*/
off(eventName, handler) {
ensureValidEventName(eventName);
ensureAllowedEventName(this[p.allowedEvents], eventName);
ensureValidHandler(handler);
assertValidEventName(eventName);
assertAllowedEventName(this[p.allowedEvents], eventName);
assertValidHandler(handler);

const handlers = this[p.listeners].get(eventName);
if (!handlers) {
Expand All @@ -169,16 +157,17 @@ export default class EventDispatcher {

/**
* Removes all registered listeners for the specified event.
* @param {string} eventName Name of the event to remove all listeners for.
*
* @param {string=} eventName Name of the event to remove all listeners for.
*/
offAll(eventName) {
if (typeof eventName === 'undefined') {
this[p.listeners].clear();
return;
}

ensureValidEventName(eventName);
ensureAllowedEventName(this[p.allowedEvents], eventName);
assertValidEventName(eventName);
assertAllowedEventName(this[p.allowedEvents], eventName);

const handlers = this[p.listeners].get(eventName);
if (!handlers) {
Expand All @@ -193,55 +182,39 @@ export default class EventDispatcher {
/**
* Emits specified event so that all registered handlers will be called
* with the specified parameters.
*
* @param {string} eventName Name of the event to call handlers for.
* @param {Object} parameters Optional parameters that will be passed to
* @param {Object=} parameters Optional parameters that will be passed to
* every registered handler.
*/
emit(eventName, parameters) {
ensureValidEventName(eventName);
ensureAllowedEventName(this[p.allowedEvents], eventName);
assertValidEventName(eventName);
assertAllowedEventName(this[p.allowedEvents], eventName);

const handlers = this[p.listeners].get(eventName);
if (!handlers) {
return;
}

handlers.forEach(function(handler) {
handlers.forEach((handler) => {
try {
handler(parameters);
handler.call(this, parameters);
} catch (error) {
console.error(error);
}
});
}
}

/**
* Mixes dispatcher methods into target object.
* @param {Object} target Object to mix dispatcher methods into.
* @param {Array.<string>} allowedEvents Optional list of the allowed event
* names that can be emitted and listened for.
* @returns {Object} Target object with added dispatcher methods.
*/
EventDispatcher.mixin = function(target, allowedEvents) {
if (!target || typeof target !== 'object') {
throw new Error('Object to mix into should be valid object!');
}
/**
* Checks if there are any listeners that listen for the specified event.
*
* @param {string} eventName Name of the event to check listeners for.
* @returns {boolean}
*/
hasListeners(eventName) {
assertValidEventName(eventName);
assertAllowedEventName(this[p.allowedEvents], eventName);

if (typeof allowedEvents !== 'undefined' &&
!Array.isArray(allowedEvents)) {
throw new Error('Allowed events should be a valid array of strings!');
return this[p.listeners].has(eventName);
}

const eventDispatcher = new EventDispatcher(allowedEvents);
Object.keys(eventDispatcher).forEach((method) => {
if (typeof target[method] !== 'undefined') {
throw new Error(
'Object to mix into already has "' + method + '" property defined!'
);
}
target[method] = eventDispatcher[method].bind(eventDispatcher);
});

return target;
};
}
Loading

0 comments on commit ee6997d

Please sign in to comment.