66

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.

Mohsen
  • 64,437
  • 34
  • 159
  • 186
NTDave
  • 663
  • 1
  • 6
  • 5
  • 1
    What's the use case? Non-DOM objects wouldn't react to events like keypresses. Why not use "normal" functions? – JJJ Mar 09 '13 at 07:31
  • I would define keydown/keyup event handlers and check for the esc key – TGH Mar 09 '13 at 07: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. – NTDave Mar 09 '13 at 07:39
  • I will refer you to this article https://developer.mozilla.org/en-US/docs/DOM/document.createEvent – Rob M. Mar 09 '13 at 07:44
  • @RobM. Are these not DOM events? – NTDave Mar 09 '13 at 07:49
  • Not necessarily, no. You can create events (using `document.createEvent` and trigger them programmatically. – Rob M. Mar 09 '13 at 07:51
  • 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. – Rob M. Mar 09 '13 at 07:53
  • Thank you so much! For some reason `document.createEvent` clicked this time C: – NTDave Mar 09 '13 at 07:55
  • You may look at this http://stackoverflow.com/questions/399867/custom-events-in-jquery – jcubic Mar 09 '13 at 08:05
  • Thanks @jcubic. I just recently found [this](http://www.west-wind.com/weblog/posts/2010/May/27/NonDom-Element-Event-Binding-with-jQuery) article on binding in JQuery. – NTDave Mar 09 '13 at 08:09
  • document.createEvent is partially depreciated and not well documented. MDN recommends CustomEvent(). https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent https://caniuse.com/#feat=customevent – SeanMC Feb 05 '19 at 20:07
  • 1
    Remember: an event is just an async function call. See the nice answer of Mr. @Mohsen about how to implement it. – 0zkr PM Jul 01 '19 at 16:17

8 Answers8

74

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

Mohsen
  • 64,437
  • 34
  • 159
  • 186
  • Impressive. Just a small point: is it fine not to use the hasOwnProperty or === in the nested foreach? `e.callbacks.forEach(...)`. – bgusach Mar 09 '13 at 08:51
  • I've never heard of this "reactor pattern" but it's exactly what I was looking for. Thanks. – NTDave Mar 09 '13 at 08:52
  • @ikaros45 I've updated it. Using an array for storing events was dump. Now it uses a hash – Mohsen Mar 09 '13 at 08:53
  • callbacks is an array, I'm just wondering if `array.forEach(...)` does not need any kind of hasOwnProperty mechanism to avoid prototypical contamination. – bgusach Mar 09 '13 at 09:08
  • 1
    @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. – Mohsen Mar 09 '13 at 19:33
  • Good to know... I wondered why I hadnt seen very often this .forEach, and it seems it is not supported by IE8, so it is not very "reliable". – bgusach Mar 09 '13 at 20:23
  • 1
    We are not writing production code for people here. BTW `forEach` has an easy polyfill – Mohsen Mar 09 '13 at 20:27
  • Worked smoothly for me.Just need to add one point, as "eventArgs" in "dispatchEvent" has to be an "array" because number of arguments for each callback may vary and hence the last line should be changed to callback.apply(null, eventArgs); – Aman Gupta Oct 28 '16 at 15:11
  • 2
    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. – BJury Jan 17 '17 at 12:22
  • @Mohsen is it possible to do this in a single es6 class? – dendog May 02 '18 at 14:41
  • @BJury could you share some more details on how to handle that issue? – dendog May 02 '18 at 15:13
  • I like the idea, so I just made a library that hope will be useful. If you have some ideas or suggestions, it will be much appreciated, feel free to commit or open a tickets ;) https://github.com/StarStep/eventer It's my first approach to GitHub so please bear with it :D – KeySee Dec 29 '18 at 23:33
  • A word of caution: this approach can result in your call stack growing very large if events are recursively being dispatched. If your code experiences this situation, an easy solution is to wrap offending dispatchEvents in a setTimeout() to allow the call stack to drain before being picked-up by the event loop. – Josh Weston Nov 28 '19 at 23:25
46

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.

Torben
  • 6,317
  • 1
  • 33
  • 32
  • 1
    +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](http://dean.edwards.name/weblog/2009/03/callbacks-vs-events/) 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. – harpo Sep 04 '16 at 17:04
  • According to MDN, this is an old-fashioned approach. Read more here https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events – psx Apr 18 '17 at 15:11
  • 3
    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. – Torben Apr 19 '17 at 20:41
  • Excellent solution! – danivicario Jun 16 '19 at 21:23
38

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

Nadeem Douba
  • 982
  • 8
  • 8
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!

Jordan Foreman
  • 3,848
  • 7
  • 40
  • 65
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.

Philipp Munin
  • 5,610
  • 7
  • 37
  • 60
  • 1
    interesting to see you can pass plain objects to the jquery constructor - your answer is definitely a good use case for this feature. details: http://api.jquery.com/jquery/#working-with-plain-objects – schellmax May 03 '16 at 15:28
  • I want to point that another way to use pub/sub with jQuery is to use the power [$.Callbacks](https://api.jquery.com/jQuery.Callbacks/) method – vsync Mar 15 '17 at 22:56
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>
Igwe Kalu
  • 14,286
  • 2
  • 29
  • 39
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!'
});
Robin van Baalen
  • 3,632
  • 2
  • 21
  • 35
-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>
Cody Gray - on strike
  • 239,200
  • 50
  • 490
  • 574
Leon Rom
  • 537
  • 4
  • 6