Reputation: 135197
I would like to create a custom event emitter in my client-side programs. I am referencing this (sparse) documentation for EventTarget
My implementation attempt
var Emitter = function Emitter() {
EventTarget.call(this);
};
Emitter.prototype = Object.create(EventTarget.prototype, {
constructor: {
value: Emitter
}
});
My desired usage
var e = new Emitter();
e.addEventListener("hello", function() {
console.log("hello there!");
});
e.dispatchEvent(new Event("hello"));
// "hello there!"
Where it fails
var e = new Emitter();
// TypeError: Illegal constructor
What am I doing wrong?
Update
The following is possible, but it's a hack that depends on a dummy DOMElement
var fake = document.createElement("phony");
fake.addEventListener("hello", function() { console.log("hello there!"); });
fake.dispatchEvent(new Event("hello"));
// "hello there!"
I'd like to know how to do this without having to use the dummy element
Upvotes: 45
Views: 38185
Reputation: 2043
There are 3 ways to achieve this depending on browser support.
EventTarget is now constructable, so just extend it:
class MyEventTarget extends EventTarget {
constructor(){
super()
}
}
The DOM 'Node' interface implements EventTarget, so just implement that instead:
function MyEventTarget(){
var target = document.createTextNode(null);
this.addEventListener = target.addEventListener.bind(target);
this.removeEventListener = target.removeEventListener.bind(target);
this.dispatchEvent = target.dispatchEvent.bind(target);
}
MyEventTarget.prototype = EventTarget.prototype;
Roll your own (assuming no options arg) & dispatch async:
function MyEventTarget(){
this.__events = new Map();
}
MyEventTarget.prototype = {
addEventListener(type, listener){
var listeners = this.__events.get(type);
if(!listeners){
listeners = new Set();
this.__events.set(type, listeners);
}
listeners.add(listener);
},
removeEventListener(type, listener){
var listeners = this.__events.get(type);
if(listeners){
listeners.delete(listener);
if(listeners.size === 0){
this.__events.delete(type);
}
}
},
dispatchEvent(event){
var listeners = this.__events.get(event.type);
if(listeners){
for(let listener of listeners){
setTimeout(listener.call(null, event), 0);
}
}
}
}
Replace Map()/Set() with {}/[] if required.
All 3 of these options can be tested with:
var target = new MyEventTarget();
target.addEventListener('test', (e) => {console.log(e.detail);}, false);
var event = new CustomEvent('test', {detail : 'My Test Event'});
target.dispatchEvent(event);
Any object that needs to implement your own 'EventTarget' interface can inherit it exactly as the native one does:
function Person(name){
MyEventTarget.call(this);
this.__name = name;
}
Person.prototype = {
__proto__ : MyEventTarget.prototype,
get name(){ return this.__name;}
}
Upvotes: 9
Reputation: 11382
EventTarget()
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 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!"
Upvotes: 5
Reputation: 1388
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
Upvotes: 5
Reputation: 668
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();
Upvotes: -1
Reputation: 6698
EventTarget
is now specified as constructible in the DOM living standard. It is supported in Chrome 64 (already out) and Firefox 59 (coming March 13).
Upvotes: 6
Reputation: 1702
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'))
Upvotes: -2
Reputation: 14866
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'));
Upvotes: 0
Reputation: 135197
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
Upvotes: 46
Reputation: 3491
Bergi was right about the part, that EventTarget
is just an interface and not a constructor.
There are multiple objects in js that are valid event targets. As mentioned there:
Element, document, and window are the most common event targets, but there are also others for example Websocket
. Anyway, all of them are given.
If you make a short test, you can notice few things:
EventTarget.isPrototypeOf(WebSocket); // true
var div = document.createElement("div");
EventTarget.isPrototypeOf(div.constructor); // true
typeof EventTarget // function
EventTarget() // TypeError: Illegal constructor
EventTarget
is prototype of these constructors, which is something you can't set for any other constructor (and even if you could, it wouldnt probably work). Also it is a function, but not callable one.
Now this is the time when you ask: So what is it EventTarget
good for and how can I use it?
We have 3 methods that each event emitter needs to implement and there was probably a need to bind these methods together, so we have an interface for them. Which means you can't use EventTarget
for calling purposes, but some other native functions might. This is similar like creating elements, we have document.createElement
factory method and we don't (can't) use new HTMLDivElement()
to create a new element, but we can compare constructors of two elements.
Conclusion
If you want to create custom event emitter, you always have to create some dummy object or use some that already exists. From my point of view, it doesn't matter what object it will be.
Some methods are not callable, but still can be compared as properties of objects. Therefore they are visible. EventTarget
is one of them.
Upvotes: 10
Reputation: 61666
Here is how to do it using CustomEvent
, cross-browser (fiddle):
// listen to event
window.addEventListener("say", function(e) { alert(e.detail.word); });
// create and dispatch the event
var event = document.createEvent("CustomEvent");
event.initCustomEvent('say', true, true,
{ "word": "Hello!" });
window.dispatchEvent(event);
You'd need to use window
or document
or any other existing DOM element to register listeneres and dispatch the event. EventTarget
is not a object, it's an interface. Try accessing EventTarget
in JavaScript console and you'll see that.
Upvotes: 2