JavaScript is one of the most powerful languages. It supports wide range of the programming styles and techniques, but such flexibility comes with danger. It is relatively easy for the JavaScript project to become a cluttered mess when the best practices are not followed or design patterns are applied incorrectly.
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 the 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.
Before implementing the MVC classes, the notification approach is need to be chosen. In JavaScript there is no special language construction or recommended interface on how the Publish-Subscribe pattern should be implemented. All implementations just use the function references. Most common of them is probably the EventEmitter implementation, which can be found in node.js for example. Two main methods of this implementations are “on” for adding event handler and “emit” for calling the event handlers for the specified event. The notifications can be added to the class by extending it from the EventEmitter or by adding methods that proxify calls to the internal instance of the EventEmitter.
class EventEmitter {
constructor() {
this._events = {};
}
on(evt, listener) {
(this._events[evt] || (this._events[evt] = [])).push(listener);
return this;
}
emit(evt, arg) {
(this._events[evt] || []).slice().forEach(lsn => lsn(arg));
}
}
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.
*/
class ListModel extends EventEmitter {
constructor(items) {
super();
this._items = items || [];
this._selectedIndex = -1;
}
getItems() {
return this._items.slice();
}
addItem(item) {
this._items.push(item);
this.emit('itemAdded', item);
}
removeItemAt(index) {
const item = this._items.splice(index, 1)[0];
this.emit('itemRemoved', item);
if (index === this._selectedIndex) {
this.selectedIndex = -1;
}
}
get selectedIndex () {
return this._selectedIndex;
}
set selectedIndex(index) {
const previousIndex = this._selectedIndex;
this._selectedIndex = index;
this.emit('selectedIndexChanged', previousIndex);
}
}
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.
*/
class ListView extends EventEmitter {
constructor(model, elements) {
super();
this._model = model;
this._elements = elements;
// attach model listeners
model.on('itemAdded', () => this.rebuildList())
.on('itemRemoved', () => this.rebuildList());
// attach listeners to HTML controls
elements.list.addEventListener('change',
e => this.emit('listModified', e.target.selectedIndex));
elements.addButton.addEventListener('click',
() => this.emit('addButtonClicked'));
elements.delButton.addEventListener('click',
() => this.emit('delButtonClicked'));
}
show() {
this.rebuildList();
}
rebuildList() {
const list = this._elements.list;
list.options.length = 0;
this._model.getItems().forEach(
item => list.options.add(new Option(item)));
this._model.selectedIndex = -1;
}
}
/**
* The Controller. Controller responds to user actions and
* invokes changes on the model.
*/
class ListController {
constructor(model, view) {
this._model = model;
this._view = view;
view.on('listModified', idx => this.updateSelected(idx));
view.on('addButtonClicked', () => this.addItem());
view.on('delButtonClicked', () => this.delItem());
}
addItem() {
const item = window.prompt('Add item:', '');
if (item) {
this._model.addItem(item);
}
}
delItem() {
const index = this._model.selectedIndex;
if (index !== -1) {
this._model.removeItemAt(index);
}
}
updateSelected(index) {
this._model.selectedIndex = index;
}
}
And of course, the Model, View, and Controller classes should be instantiated:
Your favourite JavaScript technologies:<br>
<select id="list" size="10" style="width: 17em"></select><br>
<button id="plusBtn"> + </button>
<button id="minusBtn"> - </button>
window.addEventListener('load', () => {
const model = new ListModel(['node.js', 'react']),
view = new ListView(model, {
'list' : document.getElementById('list'),
'addButton' : document.getElementById('plusBtn'),
'delButton' : document.getElementById('minusBtn')
}),
controller = new ListController(model, view);
view.show();
});
