/ JavaScript

Model-View-Controller (MVC) in JavaScript

Note: this post was originally published in 2006 (OMG, 11 years ago!). I've removed it recently, while replacing my site engine with Ghost, but it looks like it is still quite popular. I'm republishing it on the new blog due to the requests from the readers, who missed it. I've changed the style a bit. The code is the same as before.

View this post and comments in: Russian.

I like JavaScript because it is one of the most powerful languages in the world. It supports wide range of the programming styles and techniques, but such flexibility comes with danger - it is very easy for the JavaScript project to become messy if its best practices or design patterns are applied in a wrong way or inconsistently.

My goal for this article is to demonstrate how to apply the Model-View-Controller pattern while developing a simple JavaScript component. The component is a kind of the HTML ListBox ("select" HTML tag) control with an editable list of items: the user should be able to select and remove items and add new items into the list.

I hope that this article may be a good reading for you by itself. But it would be much better if you consider to run the examples and play with them.

The Model-View-Controller pattern requires some description here. The name of the pattern consists of the names of its actors: Model - stores an application data; View - renders Model for a client; and Controller - updates Model by reacting on client's actions. Wikipedia defines parts of the Model-View-Controller architecture as follows:

  • Model - The domain-specific representation of the information on which the application operates. The model is another name for the domain layer. Domain
    logic adds meaning to raw data (e.g., calculating if today is the user's birthday, or the totals, taxes and shipping charges for shopping cart items).
  • View - Renders the model into a form suitable for interaction, typically a user interface element. MVC is often seen in web applications, where the view is the HTML page and the code which gathers dynamic data for the page.
  • Controller - Processes and responds to events, typically user actions, and invokes changes on the model and perhaps the view.

So let's design the main classes of the component in a way which reflect the parts of this design pattern.

The data of the component is just a list of items, in which one particular item can be selected and deleted. So, the model of the component is very simple - it consists of an array and a selected item index; and here it is:

/**
 * The Model. Model stores items and notifies
 * observers about changes.
 */
function ListModel(items) {
    this._items = items;
    this._selectedIndex = -1;

    this.itemAdded = new Event(this);
    this.itemRemoved = new Event(this);
    this.selectedIndexChanged = new Event(this);
}

ListModel.prototype = {
    getItems : function () {
        return [].concat(this._items);
    },

    addItem : function (item) {
        this._items.push(item);
        this.itemAdded.notify({ item : item });
    },

    removeItemAt : function (index) {
        var item;

        item = this._items[index];
        this._items.splice(index, 1);
        this.itemRemoved.notify({ item : item });
        if (index === this._selectedIndex) {
            this.setSelectedIndex(-1);
        }
    },

    getSelectedIndex : function () {
        return this._selectedIndex;
    },

    setSelectedIndex : function (index) {
        var previousIndex;

        previousIndex = this._selectedIndex;
        this._selectedIndex = index;
        this.selectedIndexChanged.notify({ previous : previousIndex });
    }
};

Event is a simple class for implementing the Observer pattern:

function Event(sender) {
    this._sender = sender;
    this._listeners = [];
}

Event.prototype = {
    attach : function (listener) {
        this._listeners.push(listener);
    },
    notify : function (args) {
        var index;

        for (index = 0; index < this._listeners.length; index += 1) {
            this._listeners[index](this._sender, args);
        }
    }
};

Before designing the View we need to fix the UI structure of the component. There are numerous alternatives of interface, but for the purpose of this article the most simple one will suit better. Let's keep the items in a Listbox control and add two buttons nearby: "plus" button for adding items and "minus" for removing selected item. ListBox will provide us with the low-level machinery for selecting an item and navigating. A View class is tightly bound to a Controller class, which "... handles the input event from the user interface, often via a registered handler or callback" (from wikipedia.org).

Here are the View and Controller classes:

/**
 * The View. View presents the model and provides
 * the UI events. The controller is attached to these
 * events to handle the user interaction.
 */
function ListView(model, elements) {
    this._model = model;
    this._elements = elements;

    this.listModified = new Event(this);
    this.addButtonClicked = new Event(this);
    this.delButtonClicked = new Event(this);

    var _this = this;

    // attach model listeners
    this._model.itemAdded.attach(function () {
        _this.rebuildList();
    });
    this._model.itemRemoved.attach(function () {
        _this.rebuildList();
    });

    // attach listeners to HTML controls
    this._elements.list.change(function (e) {
        _this.listModified.notify({ index : e.target.selectedIndex });
    });
    this._elements.addButton.click(function () {
        _this.addButtonClicked.notify();
    });
    this._elements.delButton.click(function () {
        _this.delButtonClicked.notify();
    });
}

ListView.prototype = {
    show : function () {
        this.rebuildList();
    },

    rebuildList : function () {
        var list, items, key;

        list = this._elements.list;
        list.html('');

        items = this._model.getItems();
        for (key in items) {
            if (items.hasOwnProperty(key)) {
                list.append($('<option>' + items[key] + '</option>'));
            }
        }
        this._model.setSelectedIndex(-1);
    }
};

/**
 * The Controller. Controller responds to user actions and
 * invokes changes on the model.
 */
function ListController(model, view) {
    this._model = model;
    this._view = view;

    var _this = this;

    this._view.listModified.attach(function (sender, args) {
        _this.updateSelected(args.index);
    });

    this._view.addButtonClicked.attach(function () {
        _this.addItem();
    });

    this._view.delButtonClicked.attach(function () {
        _this.delItem();
    });
}

ListController.prototype = {
    addItem : function () {
        var item = window.prompt('Add item:', '');
        if (item) {
            this._model.addItem(item);
        }
    },

    delItem : function () {
        var index;

        index = this._model.getSelectedIndex();
        if (index !== -1) {
            this._model.removeItemAt(this._model.getSelectedIndex());
        }
    },

    updateSelected : function (index) {
        this._model.setSelectedIndex(index);
    }
};

And of course, the Model, View, and Controller classes should be instantiated:

$(function () {
    var model = new ListModel(['PHP', 'JavaScript']),
        view = new ListView(model, {
            'list' : $('#list'), 
            'addButton' : $('#plusBtn'), 
            'delButton' : $('#minusBtn')
        }),
        controller = new ListController(model, view);
    
    view.show();
});​
<select id="list" size="10"></select>
<button id="plusBtn">  +  </button>
<button id="minusBtn">  -  </button>

Here is a JSFiddle that contains this component if you want to play with it.

And if you familiar with UML, you may like the class diagram of the code:

Alex Netkachov

Alex Netkachov

Alex Netkachov is a Senior Software Developer, currently working in Central London on new generation of energy trading solutions for brokers, traders and exchanges.

Read More

Why not to stay updated if the subject is interesting? Join Telegram channel Alex@Net or follow alex_at_net on Twitter. Or just, use the comments form below.