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

Answers


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

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.


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


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()
    }
}

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;}
}

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.


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

// copyright, license and more examples see: https://github.com/subversivo58/Emitter
class Emitter extends EventTarget {
    constructor() {
        super()
        // store listeners
        this.listeners = {}
    }
    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, cb, { once: true }) : this.addEventListener(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 => {
                    return val === Fn ? removeListener(val) : val
                })
                // check number of listeners for this target ... remove target if empty
                if ( this.listeners[e].length === 0 ) {
                    delete this.listeners[e]
                }
            }    

            // use `.forEach()` to iterate all listeners for this target
            const iterate = () => {
                this.listeners[e].forEach((val, index, array) => {
                    removeListener(val)
                    // on end "loop" remove all listeners reference for this target (by target object)
                    if ( index === array.length -1 ) {
                        delete this.listeners[e]
                    }
                })
            }    

            Fn && typeof Fn === 'function' ? filter() : iterate()
        }
    }
    emit(e, d) {
        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 => {
    // yep, date is a `CustomEvent` object so use the "detail" property for get data
    console.log('first listener: ', data.detail)
})
MyEmitter.on('xyz', data => {
    // yep, date is a `CustomEvent` object so use the "detail" property for get data
    console.log('second listener: ', data.detail)
})

// 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.detail)
})

// 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'));

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


Need Your Help

cloud-init: What is the execution order of cloud-config directives?

user-data cloud-init

What is the order of the directives in the cloud-config section of a cloud-init user-data object. This is important to avoid race type conditions.

mysqldump data only

mysql mysqldump

I am looking for the syntax for dumping all data in my mysql database. I don't want any table information.