Last active
August 18, 2023 08:04
-
-
Save polarblau/d4c8d84630eea6b33e67 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
/** Base class for generic event handling. | |
* @example | |
* | |
* // Don’t forget to call `super()` if defining a constructor. | |
* class Foo extends Eventable {} | |
* let foo = new Foo(); | |
* | |
* foo.on('bar', (data) => console.log(data)); | |
* // will log “Hello world!” | |
* foo.trigger('bar', 'Hello World!'); | |
* foo.off('bar'); | |
* | |
* // Namespaced events: | |
* * foo.on('bar.mynamespace', (data) => console.log(data)); | |
* // will log “Hello world!” | |
* foo.trigger('bar', 'Hello World!'); | |
* // will remove all events in namespace | |
* foo.off('.mynamespace'); | |
*/ | |
class Eventable { | |
/** | |
* Create an eventable instance. | |
* @constructor | |
*/ | |
constructor() { | |
this.listeners = {}; | |
} | |
/** | |
* Register an event. | |
* @param {string} event - The name of the event. May be namespaced. | |
* @param {function} callback - Callback function. | |
*/ | |
on(event, callback) { | |
this.listeners[event] = this.listeners[event] || []; | |
this.listeners[event].push(callback); | |
} | |
/** | |
* Unregister an event. | |
* @param {string} event - The name of the event or namespace to unregister. | |
* @param {function} [callback] - Callback function to unregister. | |
*/ | |
off(event, callback) { | |
// Remove all handlers | |
if (event === undefined) { | |
// This should be a map but JS doesn’t provide any simple facilitiy | |
// to filter by keys (use case: namespace) hence we’ll resort to using | |
// an object instead. | |
this.listeners = {}; | |
return true; | |
} | |
// Remove all events in namespace | |
if (event.startsWith('.')) { | |
let listeners = {}; | |
// Filter out listeners for namespace | |
for (let key of Object.keys(this.listeners)) { | |
if (key.split('.')[1] !== event.replace(/^./, '')) { | |
listeners[key] = this.listeners[key]; | |
} | |
} | |
this.listeners = listeners; | |
} else { | |
let handlers = this.listeners[event]; | |
// No handlers found or defined, do nothing | |
if (!handlers || !handlers.length) return false; | |
if (callback) { | |
this.listeners[event] = handlers.filter((handler) => { | |
return typeof handler == 'function' && handler !== callback; | |
}); | |
} else { | |
this.listeners[event] = []; | |
} | |
} | |
return true; | |
} | |
/** | |
* Trigger an event. | |
* @param {string} event - The name of the event. | |
* @arg {...*} [args] - Data to be passed to callback. | |
*/ | |
trigger(event, ...args) { | |
let listeners = []; | |
for (let key of Object.keys(this.listeners)) { | |
if (key === event || key.split('.')[0] === event) { | |
listeners = listeners.concat(this.listeners[key]); | |
} | |
} | |
if (listeners && listeners.length) { | |
listeners.forEach((listener) => listener(...args)); | |
return true; | |
} | |
return false; | |
} | |
} | |
module.exports = Eventable; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let assert = require('assert'); | |
let sinon = require('sinon'); | |
let Eventable = require('../eventable'); | |
class Foo extends Eventable {} | |
describe('Eventable', function() { | |
let foo, spy; | |
beforeEach(function() { | |
foo = new Foo(); | |
spy = new sinon.spy(); | |
}) | |
describe('#on()', function() { | |
it('should register callback for event', function() { | |
foo.on('bar', spy); | |
foo.trigger('bar'); | |
assert(spy.called); | |
}); | |
}); | |
describe('#off()', function() { | |
it('should unregister a handler for event', function() { | |
foo.on('bar', spy); | |
foo.off('bar'); | |
foo.trigger('bar'); | |
assert(spy.notCalled); | |
}); | |
it('should unregister only a certain handler for an event', function() { | |
let otherSpy = new sinon.spy(); | |
foo.on('bar', spy); | |
foo.on('bar', otherSpy); | |
foo.off('bar', spy); | |
foo.trigger('bar'); | |
assert(spy.notCalled); | |
assert(otherSpy.called); | |
}); | |
it('remove all events', function() { | |
foo.on('bar', spy); | |
foo.off(); | |
foo.trigger('bar'); | |
assert(spy.notCalled); | |
}); | |
it('remove all events for namespace', function() { | |
foo.on('bar.namespace', spy); | |
foo.off('.namespace'); | |
foo.trigger('bar'); | |
assert(spy.notCalled); | |
}); | |
it('doesn\'t remove events outside of namespace', function() { | |
foo.on('bar.namespace', function() {}); | |
foo.on('bar', spy); | |
foo.off('.namespace'); | |
foo.trigger('bar'); | |
assert(spy.called); | |
}); | |
}); | |
describe('#trigger()', function() { | |
it('should not trigger if no event is defined', function() { | |
foo.on('other', spy); | |
foo.trigger('bar'); | |
assert(spy.notCalled); | |
}); | |
it('should not trigger other events', function() { | |
let otherSpy = new sinon.spy(); | |
foo.on('bar', spy); | |
foo.on('other', otherSpy); | |
foo.trigger('bar'); | |
assert(spy.called); | |
assert(otherSpy.notCalled); | |
}); | |
it('should pass data to callback', function() { | |
foo.on('bar', spy); | |
foo.trigger('bar', 'baz'); | |
assert(spy.calledWith('baz')); | |
}); | |
it('should trigger namespaced event', function() { | |
foo.on('bar.namespace', spy); | |
foo.trigger('bar.namespace'); | |
assert(spy.called); | |
}); | |
it('should trigger namespaced event without namespace', function() { | |
foo.on('bar.namespace', spy); | |
foo.trigger('bar'); | |
assert(spy.called); | |
}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"name": "eventable", | |
"version": "1.0.0", | |
"description": "Base class for event handling.", | |
"main": "eventable.js", | |
"scripts": { | |
"test": "mocha eventable_spec.js" | |
}, | |
"keywords": [ | |
"event", | |
"handling" | |
], | |
"author": "Florian Plank", | |
"license": "MIT" | |
} |
Thanks for your code, really helps me to implement a lib's event system!
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This doesn't get the attention it deserves. Now that we have decorators this is very useful. Thanks!