Custom Events in JavaScript MVC

on in Methods, Events and Scopes
Last modified on

Events are the bread and butter in UI development. They are frequently used to kick off JavaScript to deal with a user action on a web page. Any frontend programmer would easily recognize the common DOM based events like click, change, submit etc. Events are great, as they allow loose coupling in your code and help separate out logic.

With the recent advent of MVC frameworks in JavaScript, we have started loosely coupling our JavaScript through events on non-DOM stuff as well. Not only do we need events to know when a user clicked on a button and stuff, we also want events when Models change within our MVC application so that we can update our Views.

In the browser, the event objects are created automagically by the browser for us to inform about the changes to DOM elements. However, to watch for changes in different pieces of our own code; for instance, watching Models to update Views in a typical MVC setup, We have to implement our own eventing system and fire our own events. This is typically done with the help of the Observer pattern.

The observer pattern (a subset of the publish/subscribe pattern) is a software design pattern in which an object, called the subject, maintains a list of its dependents, called observers, and notifies them automatically of any state changes, usually by calling one of their methods.

Wikipedia

By definition of the Observer pattern in the MVC context, Models would be the subject or the observable and the Views are the observers. Now, let’s go ahead and create our own custom event scheme using the Observer pattern.

/**
 * Extremely naive implementation of Observer pattern
 * Its used to illustrate the concept and to basically show
 * How its done. This is not production level code.
 */

/**
 * Sample Model Constructor
 * @param {Object} fields
 */
var Model = function(fields) {
    this._fields = fields;
    this._listeners = [];
};

/**
 * Subscribe method can be used to subscribe
 * to this model to listen to changes on it.
 * @param {Function} cbk - function to invoke to inform
 */
Model.prototype.subscribe = function(cbk) {
    this._listeners.push(cbk);
};

/**
 * Setter function to change each property
 * @param {String} key - key name of the property to set
 * @param val - new value of the property
 */
Model.prototype.set = function(key, val) {
    this._fields[key] = val;
    this._listeners.forEach(function(listener) {
        listener.call(null, key, val);
    });
};

/**
 * Sample View Constructor
 * @param {Object} model - model object to observe
 */
var View = function(model) {
    this.model = model;
    this.model.subscribe(this.listener);
};

/**
 * Listener function that will receive model changes
 * @param {String} key - name of the property that changed
 * @param val - new value of the property
 */
View.prototype.listener = function(key, val) {
    console.log('Model property:', key, ' changed value to:', val);
};

// Code to Test Observer Pattern
var m = new Model({
    name: 'Ciprian',
    age: 37
});

var v = new View(m);

m.set('age', 38);

With the above implementation, we created our own event mechanism. The Models keep track of its observers in a private list (_listeners in our example) and the Views on subscription are pushed to that list. When something changes, it’s as simple as walking through that list and invoking each observer function. This is the classic way of doing custom events. It fits the above definition of the Observer pattern verbatim and works perfectly.

However, if you are feeling the itch, then here is another way of doing the same thing.

/**
 * Sample Model Constructor
 * @param {Object} fields
 */
var Model = function(fields) {
    this._fields = fields;

    /**
     * NOTICE how we dropped the internal array _listeners
     * from previous Gist and used a DOM element.
     * Also NOTE that this DOM element is not injected in the DOM.
     */
    this.domnode = document.createElement('DIV');

    /**
     * We also create a custom event using the browser provided
     * document.createEvent function now.
     * Our custom event is called 'change' accordingly.
     */
    this._evt = document.createEvent('Event');
    this._evt.initEvent('change', true, true);
};

/**
 * Setter function to change each property
 * @param {String} key - key name of the property to set
 * @param val - new value of the property
 */
Model.prototype.set = function(key, val) {
    this._fields[key] = val;

    // Simply attach the key,val to our custom event object
    this._evt.key = key;
    this._evt.val = val;

    // Dispatch our custom event
    this.domnode.dispatchEvent(this._evt);
};

/**
 * The new subscribe method.
 * It closely mimics browser based event scheme.
 * @param {String} eventName - name of the event to subscribe to
 * @param {Function} callback - callback listener of the event
 */
Model.prototype.addEventListener = function(eventName, callback) {
    this.domnode.addEventListener(eventName, callback);
};

/**
 * Sample View Constructor
 * @param {Object} model - model object to observe
 */
var View = function(model) {
    this.model = model;
    this.model.addEventListener('change', this.listener);
};

/**
 * Listener function that will receive model changes
 * @param {Object} event - Event Object
 */
View.prototype.listener = function(event) {
    console.log('Model property:', event.key, ' changed value to:', event.val);
};

var m = new Model({
    name: 'Ciprian',
    age: 37
});

var v = new View(m);

m.set('age', 38);

With the second approach, you will notice a bunch of little nuances:

  1. Models don’t keep track of observers in a private list (_listeners array is not required).
  2. The events we fire are very similar to DOM derived event objects. They can be bubbled, cancelled etc.
    (that is a moot point because they are on a single DOM node, not even in the DOM).
  3. We utilize a DOM node to fire off our custom events. This DOM node is never actually injected in the DOM. This is important as injecting nodes in DOM for events would be an expensive operation.
  4. Listening to these custom events in Views is very similar to listening to DOM derived events.

The approach in the first example is a classic, time-tested way of doing custom events. There is absolutely nothing wrong in it.

The second approach is something new, and I kind of find it neat. It works out nicely as well. The only thing necessary to keep in mind, while using the second approach, is that you can only use it in browser context as we are leveraging functions like createEventdispatchEvent of the document object, which are available only within the browser.

Related posts