68

I've created an object that has several methods. Some of these methods are asynchronous and thus I want to use events to be able to perform actions when the methods are done. To do this I tried to add the addEventListener to the object.

jsfiddle

var iSubmit = {
    addEventListener: document.addEventListener || document.attachEvent,
    dispatchEvent: document.dispatchEvent,
    fireEvent: document.fireEvent,   


    //the method below is added for completeness, but is not causing the problem.

    test: function(memo) {
        var name = "test";
        var event;
        if (document.createEvent) {
            event = document.createEvent("HTMLEvents");
            event.initEvent(name, true, true);
        } else {
            event = document.createEventObject();
            event.eventType = name;
        }
        event.eventName = name;
        event.memo = memo || { };

        if (document.createEvent) {
            try {
                document.dispatchEvent(event);
            } catch (ex) {
                iAlert.debug(ex, 'iPushError');
            }
        } else {
            document.fireEvent("on" + event.eventType, event);
        }
    }
}

iSubmit.addEventListener("test", function(e) { console.log(e); }, false);


//This call is added to have a complete test. The errors are already triggered with the line before this one.

iSubmit.test();

This will return an error: Failed to add eventlisterens: TypeError: 'addEventListener' called on an object that does not implement interface EventTarget."

Now this code will be used in a phonegap app and when I do, it is working on android/ios. During testing, however, it would be nice if I could get it to work in at least a single browser.

PS> I know I could enable bubbling and then listen to the document root, but I would like to have just a little bit OOP where each object can work on its own.

p u
  • 1,395
  • 1
  • 17
  • 30
Hugo Delsing
  • 13,803
  • 5
  • 45
  • 72
  • Nodejs has this feature. They do something like `var emitter = require('events').EventEmitter;`. But can we do like `ourObject.prototype.__proto__ = EventTarget.prototype;`? in here, regule JS ?? – Siva Tumma Jan 06 '14 at 16:44
  • Possible duplicate of [Implementing events in my own object](https://stackoverflow.com/questions/10978311/implementing-events-in-my-own-object) – rogerdpack Dec 11 '17 at 22:51

15 Answers15

55

addEventListener is intended for DOM Elements that implements certain event-related interfaces. If you want an event system on pure JavaScript objects, you are looking for a custom event system. An example would be Backbone.Events in Backbone.js. The basic idea is using an object as a hash to keep track of registered callbacks.

Personally I use this: emitter.

It's a fairly simple and elegant solution - with sweet short method names like on(), off() and emit(). you can either create new instances with new Emitter(), or use Emitter(obj) to mix event capabilities into existing objects. Note this library is written for use with a CommonJS module system, but you can use it anywhere else by removing the module.exports = ... line.

Edgar
  • 6,022
  • 8
  • 33
  • 66
Evan You
  • 2,701
  • 1
  • 24
  • 15
  • 8
    Thanks. I guess the answer is "It can't be done". Your plugin is nothing more then a callback system as you already mentioned. I'd rather rewrite my own code to work with callbacks then use a 3th party plugin. – Hugo Delsing Jan 06 '14 at 07:45
  • 5
    Well, events have to trigger callbacks, so an event system is essentially a callback system if you want to call it so. The EventEmitter pattern is very commonly used both in Node.js and large front end applications, so using a mature existing solution it not really a bad idea. – Evan You Jan 11 '14 at 04:32
  • The advantage of using native methods would be that native code (as `HTMLElement.addEventListener` usually excels in performance compared to javascript code written in javascript. – Tomáš Zato Nov 27 '14 at 21:27
  • 1
    In my case I setup a single proxy document fragment then used an identifier for the fragment and object (expando property like `'__id'=4`) to associate them. Dispatching an event on the object would actually occur on the fragment which would trigger the handlers setting the event.detail and target appropriately. This all occurred within a wrapping array-object (like jQuery) so it was straightforward. Not sure about it being faster than direct callbacks, promises or other approaches--the DOM is typically very complex and slow--but it works well, is fast enough and was easy to implement. – jimmont Dec 25 '15 at 19:20
  • What are the differences between EventEmitter, EventTarget and EventDispatcher? – uzay95 Oct 25 '17 at 11:23
  • How do objects like RTCPeerConnection and MediaStream have events? One would expect that this capability to have events on objects is exposed generally. – Sandeep Dixit Dec 11 '20 at 13:31
  • @SandeepDixit it is: `new EventTarget`, events are dispatched with `dispatchEvent` - this is fully usable. This answer is just from 6 years ago because this worked reliably across browsers. – Benjamin Gruenbaum Apr 09 '21 at 18:21
17

If you want to listen a javascript object you have three ways:

About sup/pub pattern:

You need to publish events.

About native implementations:

  • Object get/set operators is enough to listen add, remove, change, get events. Operators have good support. Problems only in IE8-. But if you want to use get/set in IE8 use Object.defineProperty but on DOM objects or use Object.defineProperty sham.
  • Object.prototype.watch has the good ES5 polyfill.
  • Proxy API needs ES Harmony support.

Object.observe example

var o = {};
Object.observe(o, function (changes) {
  changes.forEach(function (change) {
    // change.object contains changed object version
    console.log('property:', change.name, 'type:', change.type);
  });
});
o.x = 1     // property: x type: add
o.x = 2     // property: x type: update
delete o.x  // property: x type: delete
Benny Code
  • 51,456
  • 28
  • 233
  • 198
Alex
  • 11,115
  • 12
  • 51
  • 64
  • Unfortunately nothing I can use for my current problem, as there are easier solutions for the simple task I try to do. But you did show me some new things I'll try soon in another project. Thanks! – Hugo Delsing Jan 09 '14 at 07:54
  • I edit my answer, because there is `Object.observe` which works like you was needed. Let's stay here for future visitors and users. – Alex Jan 24 '14 at 15:54
  • 1
    Object.observe() is now deprecated. I've edited your post to warn other about not using it. Hope this update helps! https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe – andersfylling May 04 '16 at 20:43
17

If you don't need true event features(such as bubbling, stopPropagation), then you can implement your own events. addEventListener is just an API of the DOM, so you don't really need it for your own objects outside the DOM. If you want to create an evented pattern around an object, here's a good way to do it that does not require any extra browser APIs and should be very backwards-compatible.

Let's say you have an object where you want a bunch of events to be triggered when the dispatch method is called:

var OurDispatcher, dispatcher;

OurDispatcher = (function() {
  function OurDispatcher() {
    this.dispatchHandlers = [];
  }

  OurDispatcher.prototype.on = function(eventName, handler) {
    switch (eventName) {
      case "dispatch":
        return this.dispatchHandlers.push(handler);
      case "somethingElse":
        return alert('write something for this event :)');
    }
  };

  OurDispatcher.prototype.dispatch = function() {
    var handler, i, len, ref;
    ref = this.dispatchHandlers;
    for (i = 0, len = ref.length; i < len; i++) {
      handler = ref[i];
      setTimeout(handler, 0);
    }
  };

  return OurDispatcher;

})();

dispatcher = new OurDispatcher();

dispatcher.on("dispatch", function() {
  return document.body.innerHTML += "DISPATCHED</br>";
});

dispatcher.on("dispatch", function() {
  return document.body.innerHTML += "DISPATCHED AGAIN</br>";
});

dispatcher.dispatch();

It really doesn't have to be more complicated than that, for the most part. This way you have some decent control over your events and you don't need to worry about backward-compatibility or external libraries because everything there is widely supported. Technically, you could even do without setTimeout and handle your callbacks without any APIs. Anything else like stopPropagation() would have to be handled yourself.

https://jsfiddle.net/ozsywxer/

There are, of course, polyfills for CustomEvent, but unless I need advanced event features, I prefer to wrap my own eventing system into a "class" and extending other classes/functions with it.

Here's the CoffeeScript version, which is what the JavaScript is derived from: https://jsfiddle.net/vmkkbbxq/1/

^^ A bit easier to understand.

Ten Bitcomb
  • 2,316
  • 1
  • 25
  • 39
10

There are two problems.

First, the iSubmit.addEventListener() method is actually a method on the EventTarget DOM interface:

These are inteded for use only on DOM elements. By adding it to the iSubmit object as a method, you're calling it on an object that is not an EventTarget. This is why Chrome throws an Uncaught TypeError: Illegal invocation JavaScript error.

The first problem is critical, but if you could use EventTarget#addEventListener() your code would not work because the event is being added to iSubmit but dispatched from document. Generally, the same object's methods need to be used when attaching event listeners and dispatching events (unless you're using bubbling events, which is a different story - Note: bubbling is not restricted to JavaScript or DOM related events, for example).

Using custom events with your own objects is very normal. As Evan Yu mentioned, there are libraries for this. Here are a couple:

I have used js-signals and like it quite a bit. I have never used Wolfy87/EventEmitter, but it has a nice look to it.

Your example might look something like the following if you used js-signals

jsFiddle

var iSubmit = {
    finished: new signals.Signal(),
    test: function test(memo) {
        this.finished.dispatch(memo || {});
    }
};

iSubmit.finished.add(function(data) {
    console.log('finished:', data);
});

iSubmit.test('this is the finished data');


// alternatively
iSubmit.finished.dispatch('this is dispatched directly from the signal');
Community
  • 1
  • 1
tiffon
  • 5,040
  • 25
  • 34
10

Just speculation; I haven't tried it myself. But you can create a dummy element and fire/listen to events on the dummy element. Also, I prefer going without libraries.

function myObject(){
    //create "dummy" element
    var dummy = document.createElement('dummy');
    //method for listening for events
    this.on = function(event, func){dummy.addEventListener(event, func);};
    //you need a way to fire events
    this.fireEvent = function(event, obj){
      dummy.dispatchEvent(new CustomEvent(event, {detail: obj}));
    }
}
//now you can use the methods in the object constructor
var obj = new myObject();
obj.on("custom", function(e){console.log(e.detail.result)});
obj.fireEvent("custom", {result: "hello world!!!"});
Rey
  • 331
  • 4
  • 8
  • hey! I am using it for my prototype, hopefully it will make it to production. ps: we share few common thoughts – rvsingh42 Nov 24 '19 at 19:08
8

Here's a simple event emitter:

class EventEmitter {
    on(name, callback) {
        var callbacks = this[name];
        if (!callbacks) this[name] = [callback];
        else callbacks.push(callback);
    }

    dispatch(name, event) {
        var callbacks = this[name];
        if (callbacks) callbacks.forEach(callback => callback(event));
    }
}

Usage:

var emitter = new EventEmitter();

emitter.on('test', event => {
    console.log(event);
});

emitter.dispatch('test', 'hello world');
1polygon
  • 81
  • 1
  • 2
5

If you are in a Node.js environment then you can use Node's EventEmitter class:

CustomObject.js

const EventEmitter = require('events');

class CustomObject extends EventEmitter {
  constructor() {
    super();
  }

  doSomething() {
    const event = {message: 'Hello World!'};
    this.emit('myEventName', event);
  }
}

module.exports = CustomObject;

Usage:

const CustomObject = require('./CustomObject');

// 1. Create a new instance
const myObject = new CustomObject();

// 2. Subscribe to events with ID "myEventName"
myObject.on('myEventName', function(event) {
  console.log('Received event', event);
});

// 3. Trigger the event emitter
myObject.doSomething();

If you want to use Node's EventEmitter outside of a Node.js environment, then you can use webpack (preferably v2.2 or later) to get a bundle of your CustomClass together with an EventEmitter polyfill (built by webpack).

Here is how it works (assuming that you installed webpack globally using npm install -g webpack):

  1. Run webpack CustomObject.js bundle.js --output-library=CustomObject
  2. Include bundle.js in your HTML page (it will expose window.CustomObject)
  3. There's no step three!

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>Title</title>
    <script src="bundle.js"></script>
  </head>
  <body>
    <script>
      // 1. Create a new instance
      const myObject = new window.CustomObject();

      // 2. Subscribe to events with ID "myEventName"
      myObject.on('myEventName', function(event) {
        console.log('Received event', event);
      });

      // 3. Trigger the event emitter
      myObject.doSomething();
    </script>
  </body>
</html>
Benny Code
  • 51,456
  • 28
  • 233
  • 198
4

I have been able to achieve this by wrapping an element in javascript class. Important point is that the element does not have to exist in dom. Also, the element tag name can be anything such as the custom class name.

'''

class MyClass
{   
    
    constructor(options ) 
    {     
    
  
        this.el =  document.createElement("MyClass");//dummy element to manage events.    
        this.el.obj= this; //So that it is accessible via event.target.obj
      
    }

    addEventListener()
    {
 
        this.el.addEventListener(arguments[0],arguments[1]);

    }
    
 

     raiseEvent()
         {
//call this function or write code below when the event needs to be raised.

            var event = new Event('dataFound');
            event.data = messageData;
            this.el.dispatchEvent(event);
        }
}


let obj = new MyClass();
obj.addEventListener('dataFound',onDataFound);

function onDataFound()
{ 
console.log('onDataFound Handler called');
}

'''

Sandeep Dixit
  • 799
  • 7
  • 12
1

This article explains creating custom events: http://www.sitepoint.com/javascript-custom-events/

here is an example:

create the event -

var event = new CustomEvent(
    "newMessage",
    {
        detail: {
            message: "Hello World!",
            time: new Date(),
        },
        bubbles: true,
        cancelable: true
    }
);

assign the event to something -

document.getElementById("msgbox").dispatchEvent(event);

subscribe to the event -

document.addEventListener("newMessage", newMessageHandler, false);
Dawson Loudon
  • 6,029
  • 2
  • 27
  • 31
  • Thanks for the responds. Unfortunately creating an event isnt the problem. It's adding the listener method to an object that is causing the problem. I dont want to listen on the `document` but on `iSubmit` – Hugo Delsing Jan 03 '14 at 08:02
  • I see. Both answers on this thread may address your issue: http://stackoverflow.com/questions/3903226/how-to-add-an-eventlistener-to-an-object-in-javascript-which-will-fire-when-obje – Dawson Loudon Jan 03 '14 at 17:29
1

Usage: jsfiddle

This is a naive approach but might work for some applications:

CustomEventTarget.prototype = {

    'constructor': CustomEventTarget,

    on:   function( ev, el ) { this.eventTarget.addEventListener( ev, el ) },
    off:  function( ev, el ) { this.eventTarget.removeEventListener( ev, el ) },
    emit: function( ev ) { this.eventTarget.dispatchEvent( ev ) }

}

function CustomEventTarget() { this.eventTarget = new EventTarget }
flcoder
  • 713
  • 4
  • 14
1

Use the createElement to create a dummy element.

typescript

class Person {
    name: string
    el: HTMLElement // event listener
    constructor(name: string) {
        this.name = name
        this.el = document.createElement("Person"); // dummy element to manage events
        (this.el as any).object = this // set dummy attribute. (Optional) So that it is accessible via `event.target.object`
    }

    AddEventListener(type: string, listener: any) {
        this.el.addEventListener(type, listener)
    }

    DispatchEvent(type: string, data: any = null) {
        const event = new Event(type);
        (event as any).data = data //dummy attribute (Optional)
        this.el.dispatchEvent(event)
    }
}

const carson = new Person("Carson")
carson.AddEventListener("Say", (e: Event) => {
    const person = (e.target as any).object as Person // get dummy attribute
    const data = (e as any).data // get dummy attribute
    if (data !== undefined && data.stopImmediatePropagation === true) {
        e.stopImmediatePropagation()
    }
    console.log(`${person.name}`, data)
})
carson.AddEventListener("Say", () => {
    console.log("Say2")
})

carson.DispatchEvent("Say")
// Output:
// Carson undefined
// Say2
carson.DispatchEvent("Say", "hello world!")
// Carson hello world!
// Say2
carson.DispatchEvent("Say", {stopImmediatePropagation: true})
// Carson {stopImmediatePropagation: true}

Runnable Example

<script>
  class Person {
    constructor(name) {
      this.name = name
      this.el = document.createElement("Person") // dummy element to manage events
      this.el.object = this // set dummy attribute. (Optional) So that it is accessible via `event.target.object`
    }

    AddEventListener(type, listener) {
      this.el.addEventListener(type, listener)
    }

    DispatchEvent(type, data) {
      const event = new Event(type)
      event.data = data // set dummy attribute
      this.el.dispatchEvent(event)
    }
  }

  const carson = new Person("Carson")
  carson.AddEventListener("Say", (e) => {
    const person = e.target.object // get dummy attribute
    const data = e.data // get dummy attribute
    if (data !== undefined && data.stopImmediatePropagation === true) {
      e.stopImmediatePropagation()
    }
    console.log(`${person.name}`, data)
  })

  carson.AddEventListener("Say", (e) => {
    console.log("Say2")
  })

  carson.DispatchEvent("Say")
  carson.DispatchEvent("Say", "hello world!")
  carson.DispatchEvent("Say", {stopImmediatePropagation: true})
</script>
Carson
  • 6,105
  • 2
  • 37
  • 45
0

I think you can use Object $Deferred and promises. It'll let you do something like this:

Stacked: bind multiple handlers anywhere in the application to the same promise event.

  var request = $.ajax(url);
  request.done(function () {
  console.log('Request completed');
});

// Somewhere else in the application

   request.done(function (retrievedData) {
     $('#contentPlaceholder').html(retrievedData);
   });

Parallel tasks: ask multiple promises to return a promise which alerts of their mutual completion.

$.when(taskOne, taskTwo).done(function () {
  console.log('taskOne and taskTwo are finished');
});

Sequential tasks: execute tasks in sequential order.

 var step1, step2, url;

 url = 'http://fiddle.jshell.net';

 step1 = $.ajax(url);

 step2 = step1.then(
   function (data) {
       var def = new $.Deferred();

       setTimeout(function () {
           console.log('Request completed');
           def.resolve();
       },2000);

     return def.promise();

 },
   function (err) {
       console.log('Step1 failed: Ajax request');
   }
 );
 step2.done(function () {
     console.log('Sequence completed')
     setTimeout("console.log('end')",1000);
 });

Source here: http://blog.mediumequalsmessage.com/promise-deferred-objects-in-javascript-pt2-practical-use

JSG33kC0d3
  • 209
  • 4
  • 9
  • Unfortunately none of these use events. They all just do something and then use a callback function when they're ready. I could alter the iSubmit object to include callbacks, but i'd rather use events. – Hugo Delsing Jan 02 '14 at 09:03
0

Here is how you do this with Node.js style syntax in the browser.

The Events class:

  • stores callbacks in a hash associated with event keys
  • triggers the callbacks with the provided parameters

To add the behavior to your own custom classes just extend the Events object (example below).

class Events {
  constructor () {
    this._callbacks = {}
  }

  on (key, callback) {
    // create an empty array for the event key
    if (this._callbacks[key] === undefined) { this._callbacks[key] = [] }
    // save the callback in the array for the event key
    this._callbacks[key].push(callback)
  }

  emit (key, ...params) {
    // if the key exists
    if (this._callbacks[key] !== undefined) {
      // iterate through the callbacks for the event key
      for (let i=0; i<this._callbacks[key].length; i++) {
        // trigger the callbacks with all provided params
        this._callbacks[key][i](...params)
      }
    }
  }
}


// EXAMPLE USAGE

class Thing extends Events {
  constructor () {
    super()
    setInterval(() => {
      this.emit('hello', 'world')
    }, 1000)
  }
}

const thing = new Thing()

thing.on('hello', (data) => {
  console.log(`hello ${data}`)
})

Here is a link a github gist with this code: https://gist.github.com/alextaujenis/0dc81cf4d56513657f685a22bf74893d

alextaujenis
  • 151
  • 1
  • 4
0

For anyone that's looking for an easy answer that works. I visited this document, only to learn that most browser doesn't support it. But at the bottom of the page, there was a link to this GitHub page that basically does what the Object.watch() and Object.unwatch() would have done, and it works for me!

Here's how you can watch for changes

/*
 * object.watch polyfill
 *
 * 2012-04-03
 *
 * By Eli Grey, http://eligrey.com
 * Public Domain.
 * NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
 * https://gist.github.com/eligrey/384583
 */

// object.watch
if (!Object.prototype.watch) {
    Object.defineProperty(Object.prototype, "watch", {
          enumerable: false
        , configurable: true
        , writable: false
        , value: function (prop, handler) {
            var
              oldval = this[prop]
            , newval = oldval
            , getter = function () {
                return newval;
            }
            , setter = function (val) {
                oldval = newval;
                return newval = handler.call(this, prop, oldval, val);
            }
            ;

            if (delete this[prop]) { // can't watch constants
                Object.defineProperty(this, prop, {
                      get: getter
                    , set: setter
                    , enumerable: true
                    , configurable: true
                });
            }
        }
    });
}

// object.unwatch
if (!Object.prototype.unwatch) {
    Object.defineProperty(Object.prototype, "unwatch", {
          enumerable: false
        , configurable: true
        , writable: false
        , value: function (prop) {
            var val = this[prop];
            delete this[prop]; // remove accessors
            this[prop] = val;
        }
    });
}

And this should be your code:

var object = {
    value: null,
    changeValue: function(newValue) {
        this.value = newValue;
    },
    onChange: function(callback) {
        this.watch('value', function(obj, oldVal, newVal) {
            // obj will return the object that received a change
            // oldVal is the old value from the object
            // newVal is the new value from the object

            callback();
            console.log("Object "+obj+"'s value got updated from '"+oldValue+"' to '"+newValue+"'");
            // object.changeValue("hello world");
            // returns "Object object.value's value got updated from 'null' to 'hello world'";

            // and if you want the function to stop checking for
            // changes you can always unwatch it with:
            this.unwatch('value');

            // you can retrieve information such as old value, new value
            // and the object with the .watch() method, learn more here:
            // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch
        })
    }
};

or as short as:

var object = { user: null };

// add a watch to 'user' value from object
object.watch('user', function() {
    // object user value changed
});
Mxlvin
  • 288
  • 3
  • 10
-1

With ES6 class, object & callbacks you can create your own custom event system with the following code:

class ClassWithEvent {

    //Register a new event for the class
    RegisterEvent(event,Handler){
        var eventName = `event_on${event}`;
        if(this.hasOwnProperty(eventName) == false){ 
            this[eventName] = []; 
        }

        this[eventName].push(Handler);
    }

    //private unregister the event
    #unregisterEvent(event){
        var eventName = `event_on${event}`;
        delete this[eventName];
    }

    //raise event
    #dispatchEvent(name, event) {
        var eventName = `event_on${name}`;

        if (this.hasOwnProperty(eventName)) 
            this[eventName].forEach(callback => callback(event));
    }

    //public method
    sayhello(name){
        this.#dispatchEvent("beforehello",{'name':name,'method':'sayhello'});
        alert(`Hello ${name}`);
        this.#dispatchEvent("afterhello",{'name':name,'method':'sayhello'});
    }
}//EOC

Once defined you can call it as:

var ev = new ClassWithEvent();
ev.RegisterEvent("beforehello",(x)=> console.log(`Event1:before ${x.name} ${x.method} oh`));
ev.RegisterEvent("afterhello",(x)=> console.log(`Event2:after ${x.name} ${x.method} oh`));
ev.RegisterEvent("beforehello",(x)=> console.log(`Event3:before ${x.name} ${x.method} oh`));
ev.sayhello("vinod");

So in the code above we have registered 3 events handlers which will be invoked by #dispatchEvent() when we call the sayhello() method.

The instance of the class will look something like this:

enter image description here

We can see in the image above the onbeforehello event has two handlers and it will be invoke in the sequence it is defined.

So when you first run the above code, 2 events registered on before get executed. enter image description here

and the after event executes once you click ok enter image description here


Vinod Srivastav
  • 3,644
  • 1
  • 27
  • 40