I would like to create a custom event emitter in my client-side programs. I am referencing this (sparse) documentation for EventTarget
My implementation atte
Without taking into consideration browser support where EventTarget
can not be instantiated as a constructor and only to enrich this issue with yet another functional example.
According to the compatibility list described by Mozilla itself in this date (October 7, 2018):
EventTarget (constructor):
Extends:
class Emitter extends EventTarget {
constructor() {
super()
}
}
You could create common methods in many event plugins like: on()
, off()
, .once()
and emit()
(using CustomEvent
):
/**
* Emmiter - Event Emitter
* @license The MIT License (MIT) - [https://github.com/subversivo58/Emitter/blob/master/LICENSE]
* @copyright Copyright (c) 2020 Lauro Moraes - [https://github.com/subversivo58]
* @version 0.1.0 [development stage] - [https://github.com/subversivo58/Emitter/blob/master/VERSIONING.md]
*/
const sticky = Symbol()
class Emitter extends EventTarget {
constructor() {
super()
// store listeners (by callback)
this.listeners = {
'*': [] // pre alocate for all (wildcard)
}
// l = listener, c = callback, e = event
this[sticky] = (l, c, e) => {
// dispatch for same "callback" listed (k)
l in this.listeners ? this.listeners[l].forEach(k => k === c ? k(e.detail) : null) : null
}
}
on(e, cb, once = false) {
// store one-by-one registered listeners
!this.listeners[e] ? this.listeners[e] = [cb] : this.listeners[e].push(cb);
// check `.once()` ... callback `CustomEvent`
once ? this.addEventListener(e, this[sticky].bind(this, e, cb), { once: true }) : this.addEventListener(e, this[sticky].bind(this, e, cb))
}
off(e, Fn = false) {
if ( this.listeners[e] ) {
// remove listener (include ".once()")
let removeListener = target => {
this.removeEventListener(e, target)
}
// use `.filter()` to remove expecific event(s) associated to this callback
const filter = () => {
this.listeners[e] = this.listeners[e].filter(val => val === Fn ? removeListener(val) : val);
// check number of listeners for this target ... remove target if empty
this.listeners[e].length === 0 ? e !== '*' ? delete this.listeners[e] : null : null
}
// use `while()` to iterate all listeners for this target
const iterate = () => {
let len = this.listeners[e].length;
while (len--) {
removeListener(this.listeners[e][len])
}
// remove all listeners references (callbacks) for this target (by target object)
e !== '*' ? delete this.listeners[e] : this.listeners[e] = []
}
Fn && typeof Fn === 'function' ? filter() : iterate()
}
}
emit(e, d) {
this.listeners['*'].length > 0 ? this.dispatchEvent(new CustomEvent('*', {detail: d})) : null;
this.dispatchEvent(new CustomEvent(e, {detail: d}))
}
once(e, cb) {
this.on(e, cb, true)
}
}
const MyEmitter = new Emitter()
// one or more listeners for same target ...
MyEmitter.on('xyz', data => {
console.log('first listener: ', data)
})
MyEmitter.on('xyz', data => {
console.log('second listener: ', data)
})
// fire event for this target
MyEmitter.emit('xyz', 'zzzzzzzzzz...') // see listeners show
// stop all listeners for this target
MyEmitter.off('xyz')
// try new "emit" listener event ?
MyEmitter.emit('xyz', 'bu bu bu') // nothing ;)
// fire a "once" ? Yes, fire
MyEmitter.once('abc', data => {
console.log('fired by "once": ', data)
})
// run
MyEmitter.emit('abc', 'Hello World') // its show listener only once
// test "once" again
MyEmitter.emit('abc', 'Hello World') // nothing
Try my simple ES6 implemetation.
class DOMEventTarget {
constructor() {
this.listeners = new Map();
}
addEventListener(type, listener) {
this.listeners.set(listener.bind(this), {
type, listener
});
}
removeEventListener(type, listener) {
for(let [key, value] of this.listeners){
if(value.type !== type || listener !== value.listener){
continue;
}
this.listeners.delete(key);
}
}
dispatchEvent(event) {
Object.defineProperty(event, 'target',{value: this});
this['on' + event.type] && this['on' + event.type](event);
for (let [key, value] of this.listeners) {
if (value.type !== event.type) {
continue;
}
key(event);
}
}
}
let eventEmitter = new DOMEventTarget();
eventEmitter.addEventListener('test', e => {
console.log('addEventListener works');
});
eventEmitter.ontest = e => console.log('ontype works');
eventEmitter.dispatchEvent(new Event('test'));
I gave up on this awhile ago, but recently needed it again. Here's what I ended up using.
ES6
class Emitter {
constructor() {
var delegate = document.createDocumentFragment();
[
'addEventListener',
'dispatchEvent',
'removeEventListener'
].forEach(f =>
this[f] = (...xs) => delegate[f](...xs)
)
}
}
// sample class to use Emitter
class Example extends Emitter {}
// run it
var e = new Example()
e.addEventListener('something', event => console.log(event))
e.dispatchEvent(new Event('something'))
ES5
function Emitter() {
var eventTarget = document.createDocumentFragment()
function delegate(method) {
this[method] = eventTarget[method].bind(eventTarget)
}
[
"addEventListener",
"dispatchEvent",
"removeEventListener"
].forEach(delegate, this)
}
// sample class to use it
function Example() {
Emitter.call(this)
}
// run it
var e = new Example()
e.addEventListener("something", function(event) {
console.log(event)
})
e.dispatchEvent(new Event("something"))
Yeah!
For those that need to support older versions of ecmascript, here you go
// IE < 9 compatible
function Emitter() {
var eventTarget = document.createDocumentFragment();
function addEventListener(type, listener, useCapture, wantsUntrusted) {
return eventTarget.addEventListener(type, listener, useCapture, wantsUntrusted);
}
function dispatchEvent(event) {
return eventTarget.dispatchEvent(event);
}
function removeEventListener(type, listener, useCapture) {
return eventTarget.removeEventListener(type, listener, useCapture);
}
this.addEventListener = addEventListener;
this.dispatchEvent = dispatchEvent;
this.removeEventListener = removeEventListener;
}
The usage stays the same
There are two ways to implement the EventTarget "Interface".
1) Like mdn suggests use javascript prototypes. In my opinion this is clearly not the best approach to do this. The simple reason is that everybody who does use your library has to know that he needs to add a listeners
property to his constructor function.
function implement_event_target_interface(target_constructor_function)
{
target_constructor_function.prototype.listeners = null;
target_constructor_function.prototype.addEventListener = function(type, callback) {
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
};
target_constructor_function.prototype.removeEventListener = function(type, callback) {
if (!(type in this.listeners)) {
return;
}
var stack = this.listeners[type];
for (var i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback){
stack.splice(i, 1);
return;
}
}
};
target_constructor_function.prototype.dispatchEvent = function(event) {
if (!(event.type in this.listeners)) {
return true;
}
var stack = this.listeners[event.type].slice();
for (var i = 0, l = stack.length; i < l; i++) {
stack[i].call(this, event);
}
return !event.defaultPrevented;
};
}
let Person = function()
{
this.listeners = {}; // Every contructor that implements the event_target_interface must have this property. This is not very practical and intuitive for the library-user.
this.send_event = function() {
var event = new CustomEvent('test_event', { 'detail': "test_detail" });
this.dispatchEvent(event);
}
}
implement_event_target_interface(Person);
let person = new Person();
person.addEventListener('test_event', function (e) {
console.log("catched test_event from person")
}.bind(this), false);
person.send_event();
And not only that, it gets even worse when you use constructor inheritance on Person
, because you also need to inherit the prototype in order to be able to send events.
let Student = function() {
Person.call(this);
}
Student.prototype = Person.prototype;
Student.prototype.constructor = Student;
let student = new Student();
student.addEventListener('test_event', function (e) {
console.log("catched test_event from student")
}.bind(this), false);
student.send_event();
2) Use constructor inheritance. Much much better.
function EventTarget()
{
this.listeners = {};
this.addEventListener = function(type, callback) {
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
};
this.removeEventListener = function(type, callback) {
if (!(type in this.listeners)) {
return;
}
var stack = this.listeners[type];
for (var i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback){
stack.splice(i, 1);
return;
}
}
};
this.dispatchEvent = function(event) {
if (!(event.type in this.listeners)) {
return true;
}
var stack = this.listeners[event.type].slice();
for (var i = 0, l = stack.length; i < l; i++) {
stack[i].call(this, event);
}
return !event.defaultPrevented;
};
}
let Person = function()
{
EventTarget.call(this);
this.send_event = function() {
var event = new CustomEvent('test_event', { 'detail': "test_detail" });
this.dispatchEvent(event);
}
}
let person = new Person();
person.addEventListener('test_event', function (e) {
console.log("catched test_event from person")
}.bind(this), false);
person.send_event();
sample code snippet to use javascript EventTarget
// attach event
var ev = EventTarget.prototype.addEventListener.call(null, 'alert', () => alert('ALERTED'))
// dispatch event
ev.dispatchEvent.call(null, new Event('alert'))
EventType()
constructor is now supported in most modern browsers.
For the browsers which still do not support it, there is a polyfill available.
This means that it's as simple as:
var e = new EventTarget();
e.addEventListener("hello", function() {
console.log("hello there!");
});
e.dispatchEvent(new CustomEvent("hello"));
// "hello there!"
For Internet Explorer, which doesn't support CustomEvent
being used this way, there is code for a polyfill listen on the MDN page or a package on GitHub and npm
For the sake of completeness, in Node or an Electron app you would do
var EventEmitter = require('events');
var e = new EventEmitter();
e.addListener("hello", function() {
console.log("hello there!");
});
e.emit("hello")
// "hello there!"