Skip to content

Instantly share code, notes, and snippets.

@polarblau
Last active August 18, 2023 08:04
Show Gist options
  • Save polarblau/d4c8d84630eea6b33e67 to your computer and use it in GitHub Desktop.
Save polarblau/d4c8d84630eea6b33e67 to your computer and use it in GitHub Desktop.
'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;
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);
});
});
});
{
"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"
}
@Paper-Folding
Copy link

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