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.
Figure 13: Todos presenter and its component classes.
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.
function Todos() {
"use strict";
this.inherit(Todos, Controller);
var self = this,
settings = new TodoConfig(),
view = new TodoView(),
model = new TodoModel();
function initialize() {
view.on(subscribers);
view.render(view.commands.initContent, settings);
self.$base.init.call(self, router);
}
function showStats() {
model.getStats(function(stats) {
view.render(view.commands.showStats, stats);
view.render(view.commands.toggleAll, (stats.completed === stats.total));
});
}
this.init = function() {
return settings.init()
.then(function() {
return model.init();
}).then(function() {
return view.init();
}).then(initialize);
};
this.stateChanged = function() {
this.executeState();
view.render(view.commands.setFilter, this.getHyperlink());
showStats();
};
var router = {
default: function () {
model.find(function (results) {
view.render(view.commands.showEntries, results);
});
},
active: function () {
model.find({ completed: false }, function (results) {
view.render(view.commands.showEntries, results);
});
},
completed: function () {
model.find({ completed: true }, function (results) {
view.render(view.commands.showEntries, results);
});
}
};
var subscribers = {
todoAdd: new Subscriber(this, function (title) {
if (title.trim() === '') {
return;
}
model.add(title, new Subscriber(this, function () {
view.render(view.commands.clearNewTodo);
this.stateChanged();
}));
}),
todoEdit: new Subscriber(this, function (id) {
model.find(id, function (results) {
view.render(view.commands.editItem, id, results[0].title);
});
}),
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);
}
}),
todoEditCancel: new Subscriber(this, function (id) {
model.find(id, function (results) {
view.render(view.commands.editItemDone, id, results[0].title);
});
}),
todoRemove: new Subscriber(this, function (id, silent) {
model.remove(id, function () {
view.render(view.commands.removeItem, id);
});
if (!silent)
showStats();
}),
todoRemoveCompleted: new Subscriber(this, function () {
model.find({ completed: true }, function (results) {
results.forEach(function (item) {
subscribers.todoRemove(item.id, true);
});
});
showStats();
}),
todoToggle: new Subscriber(this, function (viewdata, silent) {
model.save(viewdata, function (item) {
view.render(view.commands.completedItem, item.id, item.completed);
});
if (!silent)
showStats();
}),
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();
})
};
}
Figure 14: Todos.js.
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
.
Figure 15: TodoMVC data access classes.
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.
Figure 16: TodoMVC uses one key per collection localStorage strategy.
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.
function TodoModel() {
"use strict";
this.inherit(TodoModel, Model);
var self = this,
DBNAME = 'todos';
this.init = function() {
return self.$base.init.call(self, DBNAME);
};
this.add = function(title, callback) {
title = title || '';
var todo = new TodoItem(title);
self.$base.add.call(self, todo, callback);
};
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);
};
this.save = function(entity, callback) {
var todo = new TodoItem(entity.id, entity.title, entity.completed);
self.$base.save.call(self, todo, callback);
};
}
Figure 17: TodoModel.js.
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.
Figure 18: TodoMVC presentation classes.
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.
function Template() {
"use strict";
this.inherit(Template);
var noop = function() {},
templateCache = Object.create(null);
function getAbsoluteUrl(relativeUrl) {
var prefixIndex = window.location.href.lastIndexOf('/'),
prefix = window.location.href.slice(0, prefixIndex+1);
return prefix + relativeUrl;
}
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);
}
});
}
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);
}
});
}
function loadTemplateFromUrl(source) {
var lastSeparator = source.lastIndexOf('.'),
name = source.substr(0, lastSeparator),
ext = source.substr(lastSeparator) || '.html';
return new Promise(function(resolve, reject) {
try {
$.ajax({
url: name + ext,
dataType: 'text'
})
.done(function(data) {
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);
}
});
}
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);
}
}
function getTemplate(name) {
return templateCache[name];
}
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
});
});
});
};
this.createTextFor = function(name, data) {
if (!name) return;
var template = getTemplate(name);
return template(data);
};
this.createElementFor = function(name, data) {
var html = this.createTextFor(name, data);
var d = document.createElement("div");
d.innerHTML = html;
return $(d.children);
};
}
Figure 19: Template.js.
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.
function TodoTemplate() {
'use strict';
this.inherit(TodoTemplate, Template);
var self = this;
function initialize(source) {
return self.$base.init.call(self, source)
.catch(
function (reason) {
console.log('Template cache load failure: ' + reason.message);
});
}
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>'
};
this.init = function() {
return initialize(templates);
};
}
Figure 20: TodoMVC templates defined as an object in TodoTemplate.js.
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>
Figure 21: TodoMVC template definitions embedded in the HTML using <template> elements.
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>
Figure 22: Server template file, template/template.html, contains the template definitions.
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.
Figure 23: Structure 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 base Dispatcher
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.
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');
},
clearNewTodo: function () {
view.newTodo.val('');
},
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);
},
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();
},
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);
},
removeItem: function (id) {
var item = view.todoList.find('[data-id="' + id + '"]');
item.remove();
}
};
function initialize() {
self.$base.init.call(self, viewCommands);
}
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'));
});
view.todoList.on('dblclick', 'li label', function(event) {
var id = $(event.target).parents('li').data('id');
self.trigger(self.messages.todoEdit, id);
});
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')});
});
view.todoList.on('keypress', 'li .edit', function(event) {
if (event.keyCode === self.ENTER_KEY) {
$(event.target).blur();
}
});
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);
}
});
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());
}
});
view.todoList.on('click', '.destroy', function(event) {
var id = $(event.target).parents('li').data('id');
self.trigger(self.messages.todoRemove, id);
});
}
this.init = function() {
return template.init()
.then(initialize);
};
}
Figure 24: TodoView.js.
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.
Figure 25: TodoMVC 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) {
resolve(value);
reject(error);
});
}
Figure 26: Asynchronous function returning a Promise.
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.
function InnerClass() {
"use strict";
this.inherit(InnerClass);
var self = this,
class1 = new Class1();
function initialize() {
...
}
this.init = function() {
return Class1.init()
.then(initialize);
};
}
function OuterClass() {
"use strict";
this.inherit(OuterClass);
var self = this,
innerClass = new InnerClass(),
class2 = new Class2(),
class3 = new Class3();
function initialize() {
...
}
this.init = function() {
return InnerClass.init()
.then(function() {
return class2.init();
}).then(function() {
return class3.init();
}).then(initialize);
};
}
Figure 27: The implementation of the asynchronous initialization pattern.
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 = {
...
};
function initialize() {
self.$base.init.call(self, viewCommands);
}
this.init = function() {
return template.init()
.then(initialize);
};
}
Figure 28: Implementation of the initialization pattern for TodoView.
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.
function Todos() {
"use strict";
this.inherit(Todos, Controller);
var self = this,
settings = new TodoConfig(),
view = new TodoView(),
model = new TodoModel();
function initialize() {
view.on(subscribers);
view.render(view.commands.initContent, settings);
self.$base.init.call(self, router);
}
this.init = function() {
return settings.init()
.then(function() {
return model.init();
}).then(function() {
return view.init();
}).then(initialize);
};
}
Figure 29: Initialization pattern implementation of the Todos controller.
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.