The High-Governance of No-Framework - Part 2





5.00/5 (5 votes)
The implementation of a no-framework client application using high-level developer governance.
Introduction
Part 2 of this three-part series on The High-Governance of No-Framework, presents the implementation of TodoMVC as a no-framework application using the high-governance as described in Part 1.
Background
Part 1 of this three-part series explores the background, motivation, and architectural approach to developing a no-framework application. Part 2 presents the implementation of a no-framework application. Part 3 rebuts the arguments made against no-framework application development.
TodoMVC Implementation
The implementation classes build upon the base classes introduced in the previous section. The Todos
presenter derives from the base Controller
class that provides an extremely simple routing mechanism. Todos
encapsulates a TodoView
object to manage the view and a TodoModel
object to manage data. It also contains a TodoConfig
object that retrieves settings and other configuration values.

The Todos
class implementation encapsulates its internal dependencies rather than injecting dependencies into the class as used in the VanillaJS example. Encapsulation adheres to object-oriented principles of hiding data from clients and localizing changes.
In addition, the creation of a Todos
instance occurs separately from its initialization. This is because creation is the process of allocating memory representing the instance while initialization is the act of acquiring resources. Creation is always synchronous. Initialization, on the other hand, occurs either synchronously or asynchronously depending on the manner of resource acquisition.
/*jshint strict: true, undef: true, eqeqeq: true */
/* globals console, Promise, Subscriber, Controller, TodoConfig, TodoModel, TodoView */
/**
* The todos controller.
*
* @class
*/
function Todos() {
"use strict";
this.inherit(Todos, Controller);
var self = this,
settings = new TodoConfig(),
view = new TodoView(),
model = new TodoModel();
/**
* Initialize instance.
*/
function initialize() {
view.on(subscribers);
view.render(view.commands.initContent, settings);
self.$base.init.call(self, router);
}
/**
* Display the remaining number of todo items.
*/
function showStats() {
model.getStats(function(stats) {
view.render(view.commands.showStats, stats);
view.render(view.commands.toggleAll, (stats.completed === stats.total));
});
}
/**
* Initialize the todos controller.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
// Chained initialization: settings->model->view->initialize.
return settings.init()
.then(function() {
return model.init();
}).then(function() {
return view.init();
}).then(initialize);
// Parallelize initialization: (settings||model||view)->initialize.
// Comment the chained implementation then uncomment the section below.
// return Promise.all([settings.init(),
// model.init(),
// view.init()])
// .then(initialize);
};
/**
* Set the application state.
*
* @param {string} '' | 'active' | 'completed'
*/
this.stateChanged = function() {
this.executeState();
view.render(view.commands.setFilter, this.getHyperlink());
showStats();
};
/**
* Router is the receiver of events that changes the application state.
*/
var router = {
/**
* Renders all todo list items.
*/
default: function () {
model.find(function (results) {
view.render(view.commands.showEntries, results);
});
},
/**
* Renders all active tasks
*/
active: function () {
model.find({ completed: false }, function (results) {
view.render(view.commands.showEntries, results);
});
},
/**
* Renders all completed tasks
*/
completed: function () {
model.find({ completed: true }, function (results) {
view.render(view.commands.showEntries, results);
});
}
};
/**
* Subscriber of view events.
*/
var subscribers = {
/**
* Adds a new todo item to the todo list.
*/
todoAdd: new Subscriber(this, function (title) {
// Add item.
if (title.trim() === '') {
return;
}
model.add(title, new Subscriber(this, function () {
view.render(view.commands.clearNewTodo);
this.stateChanged();
}));
}),
/*
* Starts the todo item editing mode.
*/
todoEdit: new Subscriber(this, function (id) {
model.find(id, function (results) {
view.render(view.commands.editItem, id, results[0].title);
});
}),
/*
* Saves edited changes to the todo item.
*/
todoEditSave: new Subscriber(this, function (id, title) {
if (title.length !== 0) {
model.save({id: id, title: title}, function (item) {
view.render(view.commands.editItemDone, item.id, item.title);
});
} else {
subscribers.todoRemove(id);
}
}),
/*
* Cancels the todo item editing mode and restore previous value.
*/
todoEditCancel: new Subscriber(this, function (id) {
model.find(id, function (results) {
view.render(view.commands.editItemDone, id, results[0].title);
});
}),
/**
* Removes the todo item.
*/
todoRemove: new Subscriber(this, function (id, silent) {
model.remove(id, function () {
view.render(view.commands.removeItem, id);
});
if (!silent)
showStats();
}),
/**
* Removes all completed items todo items.
*/
todoRemoveCompleted: new Subscriber(this, function () {
model.find({ completed: true }, function (results) {
results.forEach(function (item) {
subscribers.todoRemove(item.id, true);
});
});
showStats();
}),
/**
* Toggles the completion of a todo item.
*/
todoToggle: new Subscriber(this, function (viewdata, silent) {
model.save(viewdata, function (item) {
view.render(view.commands.completedItem, item.id, item.completed);
});
if (!silent)
showStats();
}),
/**
* Toggles completion of all todo items.
*/
todoToggleAll: new Subscriber(this, function (completed) {
model.find({ completed: !completed }, function (results) {
results.forEach(function (item) {
subscribers.todoToggle({id: item.id,
title: item.title,
completed: completed},
true);
});
});
showStats();
})
};
}
The Todos
class defines a router
object with properties that represent the three presentation modes of default
, active
, and completed
. The default
state presents a list of both active and completed todo items; the active
state presents the list of active todo items; the completed
state presents the list of completed Todo items. The subscribers
object defines the event messages as properties which have corresponding event handlers that become triggered by the view.
The Todos
class encapsulates commands and events. The states of the router
class are defined as commands. The init
method registers commands of the Todos
class. The subscribers
object defines the event messages and the event handlers of the Todos
class. The view.on
method attaches the Todos
class subscribers to the TodoView
class that triggers events.
TodoMVC Model
In the MVP architecture, the Todos
presenter controls the model state, as Todos
initiate all actions of the model. Because of this, the TodoMVC application does not require the model to trigger events although the base Model
class accommodates it. The TodoModel
class inherits from the Model class and uses the Storage
class to perform the heavy lifting of data accessibility from localStorage
.

The todo list gets persisted within the browser’s local storage. Rather than storing each todo item one key per item, the storage strategy used by TodoMVC persists the todo list collection as a serialized JSON array. This reduces persistence to a single collection in local storage.

The queries used by TodoMVC are limited to finding a particular todo item by id, and by querying for the list of active or completed todos. If necessary, the Storage
class can be augmented to handle more complex queries, similar in style to MongoDB.
/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/* globals $, console, document, Model, Subscriber, Storage, TodoItem */
/**
* The todos storage model.
*
* @class
*/
function TodoModel() {
"use strict";
this.inherit(TodoModel, Model);
var self = this,
DBNAME = 'todos';
/**
* Initialize the model.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
return self.$base.init.call(self, DBNAME);
};
/**
* Create new todo item
*
* @param {string} [title] The title of the task
* @param {function} [callback] The callback to fire after the model is created
*/
this.add = function(title, callback) {
title = title || '';
var todo = new TodoItem(title);
self.$base.add.call(self, todo, callback);
};
/**
* Returns a count of all todos
*/
this.getStats = function(callback) {
var results = self.$base.getItems.call(self);
var stats = { active: 0, completed: 0, total: results.length};
results.forEach(function(item) {
if (item.value.completed) {
stats.completed++;
} else {
stats.active++;
}
});
callback(stats);
};
/**
* Updates a model by giving it an ID, data to update, and a callback to fire when
* the update is completed.
*
* @param {object} entity The properties to update and their new value
* @param {function} callback The callback to fire when the update is complete.
*/
this.save = function(entity, callback) {
var todo = new TodoItem(entity.id, entity.title, entity.completed);
self.$base.save.call(self, todo, callback);
};
}
TodoMVC Presentation
The presentation system coordinates the Todos
presenter with the TodoView
and TodoTemplate
classes. Todos
creates an instance of TodoView
that gets initialized with Todos
event subscribers. TodoView
receives user events and displays information to the user. TodoView
creates TodoTemplate
that constructs elements from templated content.

Templating
A template is a piece of content that is created dynamically and rendered into HTML, rather than having the content statically rendered on the view. The template engine converts templates into HTML content. The base Template
class uses the Handlebars template engine to convert templates into HTML content.
/*jshint strict: true, undef: true, laxbreak:true */
/* globals $, console, document, window, HTMLElement, Promise, Handlebars */
/**
* The base template class.
*
* @class
*/
function Template() {
"use strict";
this.inherit(Template);
var noop = function() {},
templateCache = Object.create(null);
/**
* Converts relative url path to absolute url path.
*
* @param {string} url relative url path.
*
* @returns {string} Absolute url.
*/
function getAbsoluteUrl(relativeUrl) {
var prefixIndex = window.location.href.lastIndexOf('/'),
prefix = window.location.href.slice(0, prefixIndex+1);
return prefix + relativeUrl;
}
/**
* Load the template cache from the source properties.
*
* @param {object} source Template source object.
*
* @returns {Promise} Promise object.
*/
function loadTemplateFromObject(source) {
return new Promise(function(resolve, reject) {
try {
Object.getOwnPropertyNames(source).forEach(function(name) {
templateCache[name] = Handlebars.compile(source[name]);
});
if (Object.getOwnPropertyNames(templateCache).length > 0) {
resolve();
} else {
reject({message: 'Cannot find template object'});
}
}
catch(e) {
reject(e);
}
});
}
/**
* Load the template cache from the DOM.
*
* @param {jquery} source DOM element containing templates.
*
* @returns {Promise} Promise object.
*/
function loadTemplateFromElement(source) {
return new Promise(function(resolve, reject) {
try {
source.children().each(function(index, element) {
var name = element.getAttribute('id').replace('template-', '');
templateCache[name] = Handlebars.compile(element.innerHTML);
});
if (Object.getOwnPropertyNames(templateCache).length > 0) {
resolve();
} else {
reject({message: 'Cannot find template source: (' + source.selector + ')'});
}
}
catch(e) {
reject(e);
}
});
}
/**
* Retrieve templates from url.
*
* @param {string} source The url of the tmeplates.
*
* @returns {Promise} Promise object.
*/
function loadTemplateFromUrl(source) {
var lastSeparator = source.lastIndexOf('.'),
name = source.substr(0, lastSeparator),
ext = source.substr(lastSeparator) || '.html';
return new Promise(function(resolve, reject) {
try {
// load template file.
$.ajax({
url: name + ext,
dataType: 'text'
})
.done(function(data) {
// find the template section.
var templateSection = $('#template-section');
if (!templateSection.length) {
templateSection = $(document.createElement('section'));
templateSection.attr('id', 'template-section');
}
templateSection.append($.parseHTML(data));
templateSection.children().each(function(index, element) {
var name = element.getAttribute('id').replace('template-', '');
templateCache[name] = Handlebars.compile(element.innerHTML);
});
templateSection.empty();
resolve();
})
.fail(function(xhr, textStatus, errorThrown) {
reject({xhr: xhr,
message: 'Cannot load template source: (' + getAbsoluteUrl(name + ext) + ')',
status: textStatus});
});
}
catch(e) {
reject(e);
}
});
}
/**
* Retrieve templates from url.
*
* @param {$|HTMLElement|object|string} source Template source.
* @param {function} callback Loader callback.
*
* @returns {Promise} Promise object.
*/
function loadTemplate(source) {
if (source instanceof $) {
return loadTemplateFromElement(source);
} else if (source instanceof HTMLElement) {
return loadTemplateFromElement($(source));
} else if (typeof source === "string") {
return loadTemplateFromUrl(source);
} else {
return loadTemplateFromObject(source);
}
}
/**
* Retrieves the template by name.
*
* @param {string} name template name.
*/
function getTemplate(name) {
return templateCache[name];
}
/**
* Initialize the template
*
* @param {$|HTMLElement|object|string} source Template source.
*/
this.init = function(source) {
var self = this;
return loadTemplate(source)
.then(
function (data) {
Object.getOwnPropertyNames(templateCache).forEach(function(name) {
Object.defineProperty(self, name, {
get: function() { return name; },
enumerable: true,
configurable: false
});
});
});
};
/**
* Create text using the named template.
*
* @param {string} name Template name.
* @param {object} data Template data.
*
* @returns {string} text.
*/
this.createTextFor = function(name, data) {
if (!name) return;
var template = getTemplate(name);
return template(data);
};
/**
* Create element using the named template.
*
* @param {string} name Template name.
* @param {object} data Template data.
*
* @returns {$} jQuery element.
*/
this.createElementFor = function(name, data) {
var html = this.createTextFor(name, data);
var d = document.createElement("div");
d.innerHTML = html;
return $(d.children);
};
}
TodoTemplate
supports three templating strategies:
-
Templates defined in an object.
-
Templates defined in HTML
-
Templates defined on the server.
The source
parameter used in the init
method of TodoTemplate
determines the strategy that is used to retrieve templates. If the source is an element, then the templates are obtained from HTML. If the source is an object, then the templates are retrieved from the object's properties. If the source is a string, it is assumed to be a URL path and the templates are retrieved from the server.
Templates defined in an object.
Using this strategy, the templates are defined as an object. Each individual template is identified as a property of the object.
/*jshint strict:true, undef:true, eqeqeq:true, laxbreak:true */
/*global $, console, Template */
function TodoTemplate() {
'use strict';
this.inherit(TodoTemplate, Template);
var self = this;
/**
* Initialize instance.
*/
function initialize(source) {
return self.$base.init.call(self, source)
.catch(
function (reason) {
console.log('Template cache load failure: ' + reason.message);
});
}
/**
* Template object.
*/
var templates = {
content: ''
+ '<header class="header">'
+ '<h1>todos</h1>'
+ '<input class="new-todo" placeholder="{{placeholder}}" autofocus>'
+ '</header>'
+ '<section class="workspace">'
+ '<section class="main">'
+ '<input class="toggle-all" type="checkbox">'
+ '<label for="toggle-all">{{markall}}</label>'
+ '<ul class="todo-list"></ul>'
+ '</section>'
+ '<section class="menu">'
+ '<span class="todo-count"></span>'
+ '<ul class="filters">'
+ '<li>'
+ '<a href="#/" class="selected">{{default}}</a>'
+ '</li>'
+ '<li>'
+ '<a href="#/active">{{active}}</a>'
+ '</li>'
+ '<li>'
+ '<a href="#/completed">{{completed}}</a>'
+ '</li>'
+ '</ul>'
+ '<button class="clear-completed">{{clear}}</button>'
+ '</section>'
+ '</section>',
listitem: ''
+ '<li data-id="{{id}}" class="{{completed}}">'
+ '<div class="view">'
+ '<input class="toggle" type="checkbox" {{checked}}>'
+ '<label>{{title}}</label>'
+ '<button class="destroy"></button>'
+ '</div>'
+ '</li>',
summary: '<span><strong>{{count}}</strong> item{{plural}} left</span>'
};
/**
* init()
*
* initialize templates from source.
*
* @returns {Promise} Promise used to acquire templates.
*/
this.init = function() {
return initialize(templates);
//return initialize($('#templates'));
//return initialize('template/templates.html');
};
}
Templates embedded in HTML.
Current browser versions support the <template>
element as a means for embedding templates directly into HTML. For older browsers, a developer can use the <script>
element as a surrogate for template content. TodoMVC is geared to run on browser versions that support the <template>
element. To maintain unique identifiers, template names are prepended with template-
.
<!doctype html> <html lang="en" data-framework="javascript"> <head> <meta charset="utf-8"> <title>TodoMVC</title> <link rel="stylesheet" href="css/base.css"> <link rel="stylesheet" href="css/index.css"> </head> <body> <section class="todoapp"> </section> <footer class="info"> <p>Double-click to edit a todo</p> <p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</a></p> <p>Refactored by <a href="https://github.com/cburgmer">Christoph Burgmer</a></p> <p>Part of <a href="http://todomvc.com">TodoMVC</a></p> </footer> <!-- ----------------------------------------------------------------------------- --> <!-- Content templates --> <!-- ----------------------------------------------------------------------------- --> <section id="templates"> <!-- Content template --> <template id="template-content"> <header class="header"> <h1>todos</h1> <input class="new-todo" placeholder="{{placeholder}}" autofocus> </header> <section class="workspace"> <section class="main"> <input class="toggle-all" type="checkbox"> <label for="toggle-all">{{markall}}</label> <ul class="todo-list"></ul> </section> <section class="menu"> <span class="todo-count"></span> <ul class="filters"> <li> <a href="#/" class="selected">{{default}}</a> </li> <li> <a href="#/active">{{active}}</a> </li> <li> <a href="#/completed">{{completed}}</a> </li> </ul> <button class="clear-completed">{{clear}}</button> </section> </section> </template> <!-- Todo list item template --> <template id="template-listitem"> <li data-id="{{id}}" class="{{completed}}"> <div class="view"> <input class="toggle" type="checkbox" {{checked}}> <label>{{title}}</label> <button class="destroy"></button> </div> </li> </template> <!-- Todos summary template --> <template id="template-summary"> <strong>{{count}}</strong> item{{plural}} left </template> </section> <!—- scripts definitions removed for brevity (see Index.html) --> </body> </html>
Templates hosted on the server.
A final key strategy is that templates are requested from the server. There are several ways that the templates can be organized on the server and downloaded to the client. The strategy in this version of TodoMVC organizes templates into source fragments of <template>
elements, within a single file located at the server location: template/templates.html
. Using an ajax call, the file containing the template fragments is downloaded to the client, and then these fragments are converted into usable <template>
DOM elements.
<!-- Content template --> <template id="template-content"> <header class="header"> <h1>todos</h1> <input class="new-todo" placeholder="{{placeholder}}" autofocus> </header> <section class="workspace"> <section class="main"> <input class="toggle-all" type="checkbox"> <label for="toggle-all">{{markall}}</label> <ul class="todo-list"></ul> </section> <section class="menu"> <span class="todo-count"></span> <ul class="filters"> <li> <a href="#/" class="selected">{{default}}</a> </li> <li> <a href="#/active">{{active}}</a> </li> <li> <a href="#/completed">{{completed}}</a> </li> </ul> <button class="clear-completed">{{clear}}</button> </section> </section> </template> <!-- Todo list item template --> <template id="template-listitem"> <li data-id="{{id}}" class="{{completed}}"> <div class="view"> <input class="toggle" type="checkbox" {{checked}}> <label>{{title}}</label> <button class="destroy"></button> </div> </li> </template> <!-- Todos summary template --> <template id="template-summary"> <span><strong>{{count}}</strong> item{{plural}} left</span> </template>
TodoTemplate
assembles HTML templates during runtime and it decouples HTML presentation from the view. Using this separation, TodoView
responsibilities concentrates only on visual rendering and event handling.
TodoView
TodoView
manages the application’s user interface and appearance. It renders information to the display and transforms user events into subscriber messages. TodoView
subscribers are provided by its ownership of Todos
presenter class. This establishes an operating pattern between Todos
and TodoView,
which has Todos
issuing commands to TodoView
, and also has TodoView
publishing messages to Todos
.
The UML diagrams below reveals the class hierarchy and composition of the TodoView
class.

As a descendent of the Dispatcher
class, TodoView
carries forward the implementation of incoming commands and outgoing events. The Todos
presenter issues commands to TodoView
through the render
method that uses TodoView
DOM elements to display content to the user.
When the user issues an event, the attached DOM event handler processes the user event. Afterward, TodoView
transforms the user event into an event message that is published to subscribers.
Using this event model eliminates coupling since there is complete autonomy between the publishing of event messages and their subscribers. Commands, on the other hand, have tighter coupling because the client is aware of the command provider. The combination of commands and events permits the Todos
presenter to issues commands to its dependent TodoView
. At the same time, TodoView
publishes events to Todos
through subscribers in an independent manner.
The source code below reveals how TodoView
brings it all together. Upon construction, TodoView
creates the following:
-
an empty object as a placeholder for DOM elements (
dom
) -
an instance of
TodoTemplate
-
a container object for the view commands (
viewCommands
) -
and finally, it registers
viewCommands
to its baseDispatcher
class.
The initContent
command issued by Todos
initializes TodoView
. In initContent
, DOM elements are initialized and attachHandlers
connects event handlers to the DOM elements. Those handlers process the DOM events and then transforms them into messages that are forwarded to the view subscribers. The Todos
presenter defines the view subscribers.
/*jshint strict: true, undef: true, eqeqeq: true */
/*global $, document, View, TodoTemplate */
/**
* The todo view.
*
* @class
*/
function TodoView() {
'use strict';
this.inherit(TodoView, View);
var self = this,
view = {},
todoapp = $('.todoapp'),
template = new TodoTemplate(),
viewCommands = {
initContent: function(settings) {
var element = template.createElementFor(template.content, settings.glossary);
todoapp.append(element);
view.todoList = $('.todo-list');
view.todoItemCount = $('.todo-count');
view.clearCompleted = $('.clear-completed');
view.workspace = $('.workspace');
view.main = $('.main');
view.menu = $('.menu');
view.toggleAll = $('.toggle-all');
view.newTodo = $('.new-todo');
attachHandlers();
},
showEntries: function (todos) {
view.todoList.empty();
todos.forEach(function(todo) {
var viewdata = Object.create(null);
viewdata.id = todo.id;
viewdata.title = todo.title;
if (todo.completed) {
viewdata.completed = 'completed';
viewdata.checked = 'checked';
}
var element = template.createElementFor(template.listitem, viewdata);
view.todoList.append(element);
});
},
showStats: function (stats) {
var viewdata = Object.create(null);
viewdata.count = stats.active;
viewdata.plural = (stats.active > 1) ? 's' : '';
var text = template.createTextFor(template.summary, viewdata);
view.todoItemCount.html(text);
view.workspace.css('display', (stats.total > 0) ? 'block' : 'none');
view.clearCompleted.css('display', (stats.completed > 0) ? 'block' : 'none');
},
toggleAll: function (isCompleted) {
view.toggleAll.prop('checked', isCompleted);
},
setFilter: function (href) {
view.menu.find('.filters .selected').removeClass('selected');
view.menu.find('.filters [href="' + href + '"]').addClass('selected');
},
/**
* Clears the new todo field.
*/
clearNewTodo: function () {
view.newTodo.val('');
},
/**
* Change the completion state of the todo item.
*
* @param {number} id The todo identifier.
* @param {string} title The title of the todo.
*/
completedItem: function (id, completed) {
var listItem = view.todoList.find('[data-id="' + id + '"]');
var btnCompleted = listItem.find('.toggle');
listItem[(completed) ? 'addClass' : 'removeClass']('completed');
btnCompleted.prop('checked', completed);
},
/**
* Edit todo by creating an input field used for editing.
*
* @param {number} id The todo identifier.
* @param {string} title The title of the todo.
*/
editItem: function (id, title) {
var listItem = view.todoList.find('[data-id="' + id + '"]'),
input = $(document.createElement('input'));
listItem.addClass('editing');
input.addClass('edit');
listItem.append(input);
input.val(title);
input.focus();
},
/**
* Edit of todo is completed.
*
* @param {number} id The todo identifier.
* @param {string} title The title of the todo.
*/
editItemDone: function (id, title) {
var listItem = view.todoList.find('[data-id="' + id + '"]');
listItem.find('input.edit').remove();
listItem.removeClass('editing');
listItem.removeData('canceled');
listItem.find('label').text(title);
},
/**
* Remove the todo item.
*
* @param {number} id The todo identitifier.
*/
removeItem: function (id) {
var item = view.todoList.find('[data-id="' + id + '"]');
item.remove();
}
};
/**
* Initialize instance.
*/
function initialize() {
self.$base.init.call(self, viewCommands);
}
/**
* Attaches the UI event handler to the view selectors.
*/
function attachHandlers() {
view.newTodo.on('change', function() {
self.trigger(self.messages.todoAdd, this.value);
});
view.clearCompleted.on('click', function() {
self.trigger(self.messages.todoRemoveCompleted, this, view.clearCompleted.checked);
});
view.toggleAll.on('click', function(event) {
self.trigger(self.messages.todoToggleAll, view.toggleAll.prop('checked'));
});
/**
* Initiate edit of todo item.
*
* @param {event} event Event object.
*/
view.todoList.on('dblclick', 'li label', function(event) {
var id = $(event.target).parents('li').data('id');
self.trigger(self.messages.todoEdit, id);
});
/**
* Process the toggling of the completed todo item.
*
* @param {event} event Event object.
*/
view.todoList.on('click', 'li .toggle', function(event) {
var btnCompleted = $(event.target);
var todoItem = btnCompleted.parents('li');
var label = todoItem.find('label');
self.trigger(self.messages.todoToggle, {id: todoItem.data('id'), title: label.text(), completed: btnCompleted.prop('checked')});
});
/**
* Accept and complete todo item editing.
*
* @param {event} event Event object.
*/
view.todoList.on('keypress', 'li .edit', function(event) {
if (event.keyCode === self.ENTER_KEY) {
$(event.target).blur();
}
});
/*
* Cancel todo item editing.
*/
view.todoList.on('keyup', 'li .edit', function(event) {
if (event.keyCode === self.ESCAPE_KEY) {
var editor = $(event.target);
var todoItem = editor.parents('li');
var id = todoItem.data('id');
todoItem.data('canceled', true);
editor.blur();
self.trigger(self.messages.todoEditCancel, id);
}
});
/*
* Complete todo item editing when focus is loss.
*/
view.todoList.on('blur', 'li .edit', function(event) {
var editor = $(event.target);
var todoItem = editor.parents('li');
if (!todoItem.data('canceled')) {
var id = todoItem.data('id');
self.trigger(self.messages.todoEditSave, id, editor.val());
}
});
// Remove todo item.
view.todoList.on('click', '.destroy', function(event) {
var id = $(event.target).parents('li').data('id');
self.trigger(self.messages.todoRemove, id);
});
}
/**
* Initialize the view.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
return template.init()
.then(initialize);
};
}
Initialization Pattern
The process of object construction is fundamental to object-oriented programming. During object construction, the object's memory representation is allocated and is followed by the execution of the object's constructor method. Object-oriented languages support object construction through the new
operator. This intrinsic support obscures the two distinct tasks of object construction -- object creation and object initialization.
Object creation is the process of allocating the in-memory representation of the object, and this process executes synchronously. Object initialization is the process of assigning an object's state and acquiring resources to be consumed by the object. The execution of object initialization is either synchronous or asynchronous. The new
operator uses synchronous initialization causing a tendency to perceive the object construction process as entirely synchronous.
Synchronous initialization is most suitable in cases where the initialization data is readily available and the assignment timespan is negligible. A common example is the assignment of object state from default values. Synchronous initialization becomes impractical to use, especially for an object that acquires resources during initialization. The timespan of resource acquisition is not always negligible and blocks the CPU while running synchronously. Resources obtained from the server use asynchronous requests. The synchronous initialization that issues asynchronous resource request requires a wait mechanism in order to prevent the application from continuing until the request completes. Without a wait mechanism, the application will return an uninitialized object with undefined state. In order to enable proper handling resource acquisition, a separation of the object initialization responsibilities is necessary to result in an asynchronous initialization pattern.
TodoMVC implements an alternative initialization pattern that adheres to the following rules:
-
Class constructors are restricted to only object creation and synchronous state assignment.
-
Classes perform object initialization through a method called
init
. -
The
init
method returns an async object. -
Nesting initializers ensure that initialization of inner classes occurs before outer classes.
-
Chaining of async objects guarantees the sequence order of initialization.
The figure below represents a model of the initialization pattern.

As shown in the diagram the initialization order is OuterClass.Class1
, InnerClass
, OuterClass.Class2
, OuterClass.Class3
, and OuterClass
. The client of OuterClass
starts initialization with a call to the init
method. The init
method of OuterClass
calls the init
method of all of its inner classes in an asynchronous chain. Chaining ensures the initialization call sequence of the inner siblings classes.
JavaScript Promise
JavaScript uses the Promise
class to create an asynchronous object that returns a proxy of a future result. A through discussion of Promises is beyond the scope of this article, but having a cursory knowledge helps with the explanation of the initialization pattern used in TodoMVC. To obtain a detailed explanation of Promises visit the ExploringJS website.
The Promise
constructor has a function argument called an executor. The executor is a function that contains the asynchronous operation, which is executed by the Promise object. The executor function has two callback functions as arguments: resolve
and reject
through which the Promise object respectfully returns either a successful or error result.
function asyncFunc() {
return new Promise (
function(resolve, reject) { // Promise executor.
resolve(value); // returns success value of the Promise executor.
reject(error); // returns error value in case of failure.
});
}
The code shown below demonstrates the implementation of the asynchronous initialization pattern. It shows how the initialization sequence is managed by the chaining of Promise objects. Chaining establishes a continuation sequence of operations, through the then
method of a Promise
. A continuation is a callback that executes after the Promise
executor successfully completes. The then
method itself returns a Promise
object, which enables chaining of continuations into a sequence.
/*
* Asynchronous initialization pattern – InnerClass.
*
* @class
*/
function InnerClass() {
"use strict";
this.inherit(InnerClass);
var self = this,
class1 = new Class1();
/**
* Initialize InnerClass internal state.
*/
function initialize() {
...
}
/**
* Initialize InnerClass.
*
* @returns {Promise} Initialize contained objects.
*/
this.init = function() {
return Class1.init()
.then(initialize);
};
}
/*
* Asynchronous initialization pattern – OuterClass.
*
* @class
*/
function OuterClass() {
"use strict";
this.inherit(OuterClass);
var self = this,
innerClass = new InnerClass(),
class2 = new Class2(),
class3 = new Class3();
/**
* Initialize OuterClass internal state.
*/
function initialize() {
...
}
/**
* Initialize OuterClass.
*
* @returns {Promise} Initialization of contained objects via chaining.
*/
this.init = function() {
return InnerClass.init()
.then(function() {
return class2.init();
}).then(function() {
return class3.init();
}).then(initialize);
};
}
Using this implementation pattern for the objects used in TodoMVC guarantees consistency of object initialization throughout the application. During initialization, TodoView.init
calls the init
method of TodoTemplate
, which returns a Promise
. The then
method of the Promise
returned by TodoTemplate
contains the continuation function (initialize
) that is called after the completion of the initialization of TodoTemplate
. The initialize method of TodoView
calls the base View.init
method to complete the initialization of the view.
function TodoView() {
'use strict';
this.inherit(TodoView, View);
var self = this,
view = {},
todoapp = $('.todoapp'),
template = new TodoTemplate(),
viewCommands = {
...
};
/**
* Initialize instance.
*/
function initialize() {
self.$base.init.call(self, viewCommands);
}
/**
* Initialize the view.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
return template.init()
.then(initialize);
};
}
As the outermost container object in TodoMVC, the initialization of the Todos
controller triggers initialization of all contained objects through its init
method. The Todos
controller uses chaining to sequentially initialize its contained objects. However, if the initialization of the contained objects is not dependent on sequence, then parallelizing provides an alternative approach to chaining. The Promise.all
command takes an array of promises, executes them in parallel, and waits for the completion of all of the promises before proceeding. Parallelizing object initialization enhances the performance of the initialization process and should be tested against sequential initialization to determine the approach that works best for the application.
/**
* The todos controller.
*
* @class
*/
function Todos() {
"use strict";
this.inherit(Todos, Controller);
var self = this,
settings = new TodoConfig(),
view = new TodoView(),
model = new TodoModel();
/**
* Initialize instance.
*/
function initialize() {
view.on(subscribers);
view.render(view.commands.initContent, settings);
self.$base.init.call(self, router);
}
/**
* Initialize the todos controller.
*
* @returns {Promise} Resource acquisition promise.
*/
this.init = function() {
// Chained initialization: settings->model->view->initialize.
return settings.init()
.then(function() {
return model.init();
}).then(function() {
return view.init();
}).then(initialize);
// Parallelize initialization: (settings||model||view)->initialize.
// Comment the chained implementation then uncomment the section below.
// return Promise.all([settings.init(),
// model.init(),
// view.init()])
// .then(initialize);
};
}
Using the code
Download the zip file to your local machine and extract the todomvc
folder. To run the application, open index.html
in your browser. Use your browser's debugger to analyze the application.
The file server.py
sets up a quick-and-dirty server environment. To run the application from the server do the following:
-
Install python version 3.0 or higher on your machine.
-
Open a command window.
-
Navigate to the
todomvc
folder. -
Type
python server.py
from the command line to start the server -
Use
http://localhost:8000
in the address bar of your browser to start the application.
Points of Interest
With a working implementation in place, in Part 3 we can revisit and rebut the arguments made against no-framework.
History
11 Jun 2016 Initial presentation of the no-framework implementation.
19 Jun 2016 Fixed the displayable HTML of the JavaScript code in figure 20.