68

I'm new to JavaScript and programming in general, and I have some questions about objects and events.

Say I have an object:

var computer = {
    keyboard: {}
}

What I'm looking for is a way to register events to the keyboard object:

computer.keyboard.registerEvent( "keyEscape" );

Fire the event:

computer.keyboard.dispatchEvent( "keyEscape" );

And create event handlers:

computer.keyboard.addEventListener( "keyEscape", function() {...} );

I know how to do this with DOM elements but not objects. Is this something that can be done in JavaScript (maybe with the help of JQuery)?

Even the slightest bit of guidance would be appreciated greatly.

12
  • 1
    What's the use case? Non-DOM objects wouldn't react to events like keypresses. Why not use "normal" functions? Commented Mar 9, 2013 at 7:31
  • I would define keydown/keyup event handlers and check for the esc key Commented Mar 9, 2013 at 7:32
  • @Juhana I created a "HTML" vgui panel in a source engine game, and I need to pass keypresses to the panel (browser) when it is not visible. The only way to do this is by calling Panel:RunJavaScript( "javascript" ), which runs the given javascript. The first way I could think of to do this is by registering events through RunJavaScript, and then firing them the same way. Commented Mar 9, 2013 at 7:39
  • 1
    If it's possible with jQuery, it is possible without jQuery; this is a confusing point for many new developers, jQuery is just a library of functions/objects written in javascript. Commented Mar 9, 2013 at 7:53
  • 1
    Remember: an event is just an async function call. See the nice answer of Mr. @Mohsen about how to implement it. Commented Jul 1, 2019 at 16:17

8 Answers 8

75

If you want to make a completely stand alone event system without relying on DOM events you can have something like this using reactor pattern

function Event(name){
  this.name = name;
  this.callbacks = [];
}
Event.prototype.registerCallback = function(callback){
  this.callbacks.push(callback);
}

function Reactor(){
  this.events = {};
}

Reactor.prototype.registerEvent = function(eventName){
  var event = new Event(eventName);
  this.events[eventName] = event;
};

Reactor.prototype.dispatchEvent = function(eventName, eventArgs){
  this.events[eventName].callbacks.forEach(function(callback){
    callback(eventArgs);
  });
};

Reactor.prototype.addEventListener = function(eventName, callback){
  this.events[eventName].registerCallback(callback);
};

Use it like DOM events model

var reactor = new Reactor();

reactor.registerEvent('big bang');

reactor.addEventListener('big bang', function(){
  console.log('This is big bang listener yo!');
});

reactor.addEventListener('big bang', function(){
  console.log('This is another big bang listener yo!');
});

reactor.dispatchEvent('big bang');

Live at JSBin

Sign up to request clarification or add additional context in comments.

13 Comments

Impressive. Just a small point: is it fine not to use the hasOwnProperty or === in the nested foreach? e.callbacks.forEach(...).
I've never heard of this "reactor pattern" but it's exactly what I was looking for. Thanks.
@ikaros45 It's true that Arrays are Objects in JavaScript and they can have prototypal values. If you do Array.prototype.foo = 'lol', foo will not show up in forEach loops but will show up in for...in loop.
We are not writing production code for people here. BTW forEach has an easy polyfill
A word of warning here, there is no consideration to what happens when you want to get rid of a callback. Without it you're going to have memory leaks and other problems with objects that you might expect to have been disposed actually processing an event.
|
49

You can simply create a new EventTarget instance like some have suggested without having to create a DOM object, like so:

const target = new EventTarget();
target.addEventListener('customEvent', console.log);
target.dispatchEvent(new Event('customEvent'));

This provides all the functionality you're used to with DOM events and doesn't require an empty document element or node to be created.

See the Mozilla Developer Guide for more information: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget

1 Comment

This runs in Node.js as well as in the browser.
47

Update: This approach is only valid for old browsers. For newer browsers it is better to use the EventTarget class. See Nadeem Douba's answer for details.

If you don't want to implement your own event handling mechanisms, you might like my approach. You'll get all the features you know from usual DOM Events (preventDefault() for example) and I think it's more lightweight, because it uses the already implemented DOM event handling capabilities of the browser.

Just create a normal DOM EventTarget object in the constructor of your object and pass all EventTarget interface calls to the DOM EventTarget object:

var MyEventTarget = function(options) {
    // Create a DOM EventTarget object
    var target = document.createTextNode(null);

    // Pass EventTarget interface calls to DOM EventTarget object
    this.addEventListener = target.addEventListener.bind(target);
    this.removeEventListener = target.removeEventListener.bind(target);
    this.dispatchEvent = target.dispatchEvent.bind(target);

    // Room your your constructor code 
}

// Create an instance of your event target
myTarget = new MyEventTarget();
// Add an event listener to your event target
myTarget.addEventListener("myevent", function(){alert("hello")});
// Dispatch an event from your event target
var evt = new Event('myevent');
myTarget.dispatchEvent(evt);

There is also a JSFiddle snippet to test it with your browser.

4 Comments

+1. This is the only implementation I've seen that has the expected failure mode, where an exception in one handler doesn't hose later handlers. (See this 2009 post for details.) That said, I'm not sure that this is more "lightweight" than callbacks these days, even if it uses slightly less userland code. Good idea using an empty text node; I was using a div, but this seems better somehow.
According to MDN, this is an old-fashioned approach. Read more here developer.mozilla.org/en-US/docs/Web/Guide/Events/…
The solution itself is not old-fashioned - only the way to create "myevent" was. I've updated the example code so that it uses the modern way to create events now.
Excellent solution!
13

Necroposting a little here, but I just wrote something like this last night - super simple, and based off of Backbone.js Events module:

EventDispatcher = {

    events: {},

    on: function(event, callback) {
        var handlers = this.events[event] || [];
        handlers.push(callback);
        this.events[event] = handlers;
    },

    trigger: function(event, data) {
        var handlers = this.events[event];

        if (!handlers || handlers.length < 1)
            return;

        [].forEach.call(handlers, function(handler){
            handler(data);
        });
    }
};

This approach is incredibly simple and extensible, allowing you to build a more sophisticated event system on top of it if you need.

Using the EventDispatcher is as simple as:

function initializeListeners() {
    EventDispatcher.on('fire', fire); // fire.bind(this) -- if necessary
}

function fire(x) {
    console.log(x);
}

function thingHappened(thing) {
    EventDispatcher.trigger('fire', thing);
}

With some simple namespacing, you'll be able to pass basic events between modules with ease!

3 Comments

You probably also need an off method to remove event listeners
Why [].forEach.call(handlers, ...) instead of handlers.forEach(...)? handlers is an actual Array instance.
@DavidHarkness that's a great suggestion! I answered this about four years ago, well before I had adopted the newer array methods.
5

You can do it using JQuery.

For subscribing to your custom event:

$(computer.keyboard).on('keyEscape', function(e){
    //Handler code
});

For throwing your custom event:

$(computer.keyboard).trigger('keyEscape', {keyCode:'Blah blah'});

Might be not the nicest way to do this, but you also can create functions in your method (addEventListener, dispatchEvent,...) that will wrap JQuery logic, to support both native looking api and JQuery.

2 Comments

interesting to see you can pass plain objects to the jquery constructor - your answer is definitely a good use case for this feature. details: api.jquery.com/jquery/#working-with-plain-objects
I want to point that another way to use pub/sub with jQuery is to use the power $.Callbacks method
0

Most likely, you need an event mechanism as a medium of communication among several objects.

Heres how you can achieve that:

/**
 * EventfulObject constructor/base.
 * @type EventfulObject_L7.EventfulObjectConstructor|Function
 */
var EventfulObject = function() {
  /**
   * Map from event name to a list of subscribers.
   * @type Object
   */
  var event = {};
  /**
   * List of all instances of the EventfulObject type.
   * @type Array
   */
  var instances = [];
  /**
   * @returns {EventfulObject_L1.EventfulObjectConstructor} An `EventfulObject`.
   */
  var EventfulObjectConstructor = function() {
    instances.push(this);
  };
  EventfulObjectConstructor.prototype = {
    /**
     * Broadcasts an event of the given name.
     * All instances that wish to receive a broadcast must implement the `receiveBroadcast` method, the event that is being broadcast will be passed to the implementation.
     * @param {String} name Event name.
     * @returns {undefined}
     */
    broadcast: function(name) {
      instances.forEach(function(instance) {
        (instance.hasOwnProperty("receiveBroadcast") && typeof instance["receiveBroadcast"] === "function") &&
        instance["receiveBroadcast"](name);
      });
    },
    /**
     * Emits an event of the given name only to instances that are subscribed to it.
     * @param {String} name Event name.
     * @returns {undefined}
     */
    emit: function(name) {
      event.hasOwnProperty(name) && event[name].forEach(function(subscription) {
        subscription.process.call(subscription.context);
      });
    },
    /**
     * Registers the given action as a listener to the named event.
     * This method will first create an event identified by the given name if one does not exist already.
     * @param {String} name Event name.
     * @param {Function} action Listener.
     * @returns {Function} A deregistration function for this listener.
     */
    on: function(name, action) {
      event.hasOwnProperty(name) || (event[name] = []);
      event[name].push({
        context: this,
        process: action
      });

      var subscriptionIndex = event[name].length - 1;

      return function() {
        event[name].splice(subscriptionIndex, 1);
      };
    }
  };

  return EventfulObjectConstructor;
}();

var Model = function(id) {
  EventfulObject.call(this);
  this.id = id;
  this.receiveBroadcast = function(name) {
    console.log("I smell another " + name + "; and I'm model " + this.id);
  };
};
Model.prototype = Object.create(EventfulObject.prototype);
Model.prototype.constructor = Model;

// ---------- TEST AND USAGE (hopefully it's clear enough...)
// ---------- note: I'm not testing event deregistration.

var ob1 = new EventfulObject();
ob1.on("crap", function() {
  console.log("Speaking about craps on a broadcast? - Count me out!");
});

var model1 = new Model(1);

var model2 = new Model(2);
model2.on("bust", function() {
  console.log("I'm model2 and I'm busting!");
});

var ob2 = new EventfulObject();
ob2.on("bust", function() {
  console.log("I'm ob2 - busted!!!");
});
ob2.receiveBroadcast = function() {
  console.log("If it zips, I'll catch it. - That's me ob2.");
};

console.log("start:BROADCAST\n---------------");
model1.broadcast("crap");
console.log("end  :BROADCAST\n---------------\n-\n-\n");
console.log("start:EMIT\n---------------");
ob1.emit("bust");
console.log("end:EMIT\n---------------");
<h1>...THE SHOW IS ON YOUR CONSOLE!</h1>

Comments

0

Since I came across this question almost 10 years later, a lot has changed in the browser/javascript world since most answers were given.

Here are my two cents for those of us who use Javascript module imports/exports:

// eventbus.js

class EventBus {
    constructor() {
        this.events = {};
    }

    on(type, callback) {
        if (!this.events[type]) {
            this.events[type] = [];
        }

        this.events[type].push(callback);
    }

    off(type, callback) {
        if (!this.events[type]) {
            return;
        }

        this.events[type] = this.events[type].filter(listener => listener !== callback);
    }

    dispatch(type, data) {
        if (!this.events[type]) {
            return;
        }

        this.events[type].forEach(listener => listener(data));
    }
}

export const eventbus = new EventBus();
// somefile.js
import {eventbus} from './eventbus';

// Somewhere in a method/click callback/etc..
eventbus.dispatch('fire', {message: 'Fire in the hole!'});
// otherfile.js
import {eventbus} from './eventbus';

eventbus.on('fire', data => {
    console.log(data.message); // logs 'Fire in the hole!'
});

Comments

-1

Here is a simple extension of Mohsen's answer, presented as a clear and short example.

All his React functions are encapsulated into one React(), added a function removeEventListener(), and whole example is presented as one HTML file (or see it on JSFiddle).

<!DOCTYPE html>
<html>

<head>
    <meta charset=utf-8 />
    <title>JS Bin</title>
    <!--https://jsfiddle.net/romleon/qs26o3p8/-->
</head>

<body>
    <script>
        function Reactor() {
            function Event(name) {
                this.name = name;
                this.callbacks = [];
            }
            Event.prototype.registerCallback = function(callback) {
                this.callbacks.push(callback);
            };
            Event.prototype.unregisterCallback = function(callback) {
                var array = this.callbacks,
                    index = array.indexOf(callback);
                if (index > -1)
                    array.splice(index, 1);
            }
            this.events = {};

            this.registerEvent = function(eventName) {
                var event = new Event(eventName);
                this.events[eventName] = event;
            };
            this.dispatchEvent = function(eventName, eventArgs) {
                var events = this.events
                if (events[eventName]) {
                    events[eventName].callbacks.forEach(function(callback) {
                        callback(eventArgs);
                    });
                }
                else
                    console.error("WARNING: can't dispatch " + '"' + eventName + '"')
            };
            this.addEventListener = function(eventName, callback) {
                this.events[eventName].registerCallback(callback);
            };

            this.removeEventListener = function(eventName, callback) {
                var events = this.events
                if (events[eventName]) {
                    events[eventName].unregisterCallback(callback);
                    delete events[eventName];
                }
                else
                    console.error("ERROR: can't delete " + '"' + eventName + '"')
            };
        }
/*
    demo of creating
*/
        var reactor = new Reactor();

        reactor.registerEvent('big bang');
        reactor.registerEvent('second bang');

/*
    demo of using
*/
        log("-- add 2 event's listeners for 'big bang' and 1 for 'second bang'")
        var callback1 = function() {
            log('This is big bang listener')
        }
        reactor.addEventListener('big bang', callback1);

        reactor.addEventListener('big bang', function() {
            log('This is another big bang listener')
        });

        reactor.addEventListener('second bang', function() {
            log('This is second bang!')
        });

        log("-- dipatch 'big bang' and 'second bang'")
        reactor.dispatchEvent('big bang');
        reactor.dispatchEvent('second bang');

        log("-- remove first listener (with callback1)")
        reactor.removeEventListener('big bang', callback1);

        log("-- dipatch 'big bang' and 'second bang' again")
        reactor.dispatchEvent('big bang');
        reactor.dispatchEvent('second bang');

        function log(txt) {
            document.body.innerHTML += txt + '<br/>'
            console.log(txt)
        }
    </script>
</body>

</html>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.