Custom Events Model without using DOM events in JavaScript

JavascriptEventsObjectHandler

Javascript Problem Overview


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.

Javascript Solutions


Solution 1 - Javascript

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

Solution 2 - Javascript

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.

Solution 3 - Javascript

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

Solution 4 - Javascript

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!

Solution 5 - Javascript

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.

Solution 6 - Javascript

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>

Solution 7 - Javascript

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>

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionNTDaveView Question on Stackoverflow
Solution 1 - JavascriptMohsenView Answer on Stackoverflow
Solution 2 - JavascriptTorbenView Answer on Stackoverflow
Solution 3 - JavascriptNadeem DoubaView Answer on Stackoverflow
Solution 4 - JavascriptJordan ForemanView Answer on Stackoverflow
Solution 5 - JavascriptPhilipp MuninView Answer on Stackoverflow
Solution 6 - JavascriptIgwe KaluView Answer on Stackoverflow
Solution 7 - JavascriptLeon RomView Answer on Stackoverflow