Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import Event Dispatcher and tests. r=gmarty #115

Merged
merged 1 commit into from
Apr 22, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have mixed feelings about this method - but it simplifies a lot my next PR, so I've added this one. If in the future I find better approach to know that I can shutdown the bus (or something that depends on the bus) if there is no listeners anymore, I would probably try to get rid of this method.

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