Mulan
Mulan

Reputation: 135197

How to use JavaScript EventTarget?

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

Answers (10)

Neil
Neil

Reputation: 2043

There are 3 ways to achieve this depending on browser support.

  1. EventTarget is now constructable, so just extend it:

    class MyEventTarget extends EventTarget {
        constructor(){
            super()
        }
    }
    

    example on MDN

  2. 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;
    
  3. 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

Codebling
Codebling

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

Lauro Moraes
Lauro Moraes

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):

  • desktop:
    • Chrome 64
    • Firefox 59
    • Opera 51
  • mobile:
    • WebView 64
    • Chrome Android 64
    • Firefox Android 59
    • Opera Android 51

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

Ini
Ini

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

guest
guest

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

Imamudin Naseem
Imamudin Naseem

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

Lewis
Lewis

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

Mulan
Mulan

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

Entity Black
Entity Black

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

noseratio
noseratio

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

Related Questions