A Simple Javascript Model-View-Controller API

Model-View-Controller is such a useful and powerful design pattern. You can use MVC for more than just physical views (i.e. GUI’s), but abstract views as well (e.g. pure data structures). You can even use MVC as an event mechanism.

I have written an easy-to-use MVC API for your Javascript applications. The following MVC example uses the API. The model is the userdoc, the views are the toolbar and menu, the controller is the user clicks/edits:


// The user document model
var userdoc = function() {

  // Model data
  var data = "", selStart, selEnd;

  var ud = {

    /**
    * Sets the document data.
    * @param {String} newData The new data to set.
    */
    setData : function(newData) {
      data = newData;
      this.fireEvent("DataChanged", newData);
    }
  };

  // Controller: user clicks adjusts selection model data
  document.getElementById("userdoc").onclick = function(e) {
    // Set cursor/selection logic
    ud.fireEvent("SelectionChanged", selStart, selEnd, e || window.event);
  }

  return ud;

}();

// Make the userdoc a Model
declareModel(userDoc);

// The tool bar view
var toolBar = {

  // .. Interface code...

  /**
  * Listener function
  */
  onSelectionChanged : function(selStart, selEnd) {
     // Update GUI Buttons according to selection
  }
};
userDoc.addObserver(toolBar);

// Add observer for the main menu
userDoc.addObserver({
  onDataChanged : function(data) {
    menu.saveButton.setEnabled(data != lastSavedData)
  },
  onSelectionChanged : function(selStart, selEnd) {
    // Update revelent buttons according to selection
  }
}, menu);

for (var button in menu) {
  if (button.requiresSelection) {
    // Observe the document selection state
    // and synchronize this buttons enable
    // state according to the selection state
    userDoc.addObserver({
      onSelectionChanged : function(selStart, selEnd) {
        // this points to a button - which is enabled only 
        // if there is selection in the document
        this.setEnabled(selStart ? true : false);
      }
    }, button);
  }
}

To declare a model you just invoke the declareModel function passing the model object.

Whenever model data has changed (or when ever you want to raise an event), a fireEvent function will be present in the model object. There is no need to declare event types, you can just fire which ever event name you want. For example, if you fire an event named “Click”, all attached observers which a function called onClick will be invoked along with any extra/optional arguments passed to the fireEvent call.

The context which the observer event listener functions are called defaults to the observer objects themselves, otherwise you can provide a specific object. For example; in the code above observers are added to the user document model for every button which requires selection to be enabled, so during registration of each observer the context is set as the button objects (via the second argument). In the observer event code the this identifier points to the corresponding button.

The script for the MVC API:


/**
 * Extends the given instance into a model (wrt MVC).
 * 
 * @param {Object} subject The subject containing model data to be observed.
 */
function declareModel(subject) {
    
    // The list of registered observers for this subject
    var observers = [];

    /**
     * 
     * Notifies all observers that a specific event occured.
     * Extra arguments will be passed to the observer's relevent event functions.
     * 
     * @param {String} event   The event name to fire (excluding the "on" prefix).
     *                         For example, "KeyDown" would invoke "onKeyDown" in all observers
     *                         
     */
    subject.fireEvent = function(event){
        
        if (observers.length > 0) {
            
            // Construct additional arguments array
            var args = Array.prototype.slice.call(arguments);
            args.shift();
            
            for (var i in observers) {
                var observer = observers[i];
                
                // If the observer has declared a listener function for this event invoke it
                if (typeof observer.ref["on" + event] == "function")
                	observer.ref["on" + event].apply(observer.context, args);
            }
        }
    };
    
    /**
     * Adds an observer for receiving event notifications.
     * If the observer already exists in the observer set it will not be added twice.
     * 
     * @param {Object} observer         An observer to add to the set.
     * 
     * @param {Object} context          (Optional) The context at which the events should be invoked in.
     *                                  Will default to the observer object.
     *                                  
     * @param {Boolean} notifiedFirst   (Optional) True to be the first observer to be notified in the current list.
     *                                  Otherwise it will be added to the end of the list
     */
    subject.addObserver = function(observer, context, notifiedFirst){
        
        // Ensure that observer array is a set
        if (observerIndex(observer) != -1)
            return;
            
        // Create observer instance
        observer = {
            ref : observer,
            context : context || observer
        };
        
        // Add to list depending on requested order
        if (notifiedFirst) observers.unshift(observer);
        else observers.push(observer);
        
    };
    
    /**
     * Removes an observer from the subjects observer list.
     * 
     * @param {Object} observer An observer to remove from the set
     */
    subject.removeObserver = function(observer) {
        var index = observerIndex(observer);
        if (index >= 0) observers.splice(index, 1);
    };
    
    /**
     * @param {Object} observerRef The observer reference to check
     * 
     * @return {Number} The index in the observers array at which observerRef exists.
     *                  -1 if not found.
     */
    function observerIndex(observerRef) {
        for (var i = 0; i < observers.length; i++) {
            if (observers[i].ref == observerRef)
                return i;
        }
        return -1;
    }
    
}
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s