Introduction
On a recent project, there was conflict within the development team over whether the team should adopt a popular JavaScript framework to construct front-end web clients. The controversy began when my colleague, “Dale”, an extremely proficient frontend developer, suggested that the development team take a “no-framework” approach. Needless to say, his proposal viscerally upset the other team members who were advocating for the use of AngularJS.
My initial reaction to Dale’s suggestion was to say that no-framework is a radical departure from the norm, as it seems to be common practice these days to develop web frontends using a popular JavaScript framework. But as I thought about it, I recalled from my own evaluation of AngularJS on a previous project, I had concluded that AngularJS so obscures standard web programming, that the paradigm is better described as AngularJS Programming.
I am also familiar with Backbone.js from previous projects. Unlike AngularJS, Backbone feels more natural to use. Backbone is a library that aids with the structuring of applications into the MV* architectural pattern and it leverages existing programming knowledge. Backbone does not diverge in such a manner that forces you to learn a new programming paradigm. It was for these reasons that I sided with Dale.
Today, unfortunately, rational arguments, experience, and common sense reasoning do not always prevail. Instead, sophistry often wins out. Our no-framework opposition had neatly packaged their arguments as shown below:
Business Requirements & Architectural Concerns
| jQuery + Libraries
| AngularJS
|
Data Binding
(two-way)
| Involves the use of multiple libraries, which increases instability.
This will require DOM manipulation to remove and append nodes manually. This relies on the use of element ids and classes.
| Built in support for two-way data binding.
Works with models that modify bound elements automatically.
No manual DOM manipulation is required.
No manual DOM manipulation is required.
|
Templating
| Templates are text files which are not maintainable and do not support IntelliSense.
Text templates are error prone because the templating is string based.
| Built-in support for HTML templates and directives.
Works with HTML templates with partial views, which are maintainable & support IntelliSense.
|
MVC/MVVM solution design
Such a pattern brings a clear separation of concerns, which makes the code easy to build, maintain, and test.
| Using jQuery requires defining a proprietary pattern to achieve MVC architectural design. This is time-consuming and unnecessary (reinventing the wheel).
| AngularJS has built-in MVC architecture with clearly defined modules, controllers, services, and data models.
|
Dependency Management
| No built-in dependency management.
This feature depends on the use of additional libraries like RequireJS or Inject, which increases instability.
| AngularJS has built-in dependency management.
|
Unit Testing
| Testing requires multiple libraries like QUnit and other libraries for dependency injection, etc.
| AngularJS has built-in dependency injection to mock dependencies for unit testing.
AngularJS unit tests can be written using a behavior driven design pattern in Jasmine, which is easy to maintain and read.
|
End to End Testing or Page Behavior Validation
| Not possible with jQuery. | AngularJS has an end-to-end framework designed for testing, called Protractor. |
Compliance and Licensing
| Using multiple libraries will add significant effort & costs, in terms of validating the compliance and licensing conditions for these libraries. | PCI compliant and ISO compliant businesses use AngularJS in the market, which reduces the effort and costs. |
Staffing
| Competent JavaScript developers are difficult to find. | AngularJS developers are plentiful and they have community support. |
Consistency
| JavaScript programming requires guidelines to enforce programming consistency. | AngularJS promotes a consistent programming style well suited for development teams. |
Security and Compatibility
| The use of multiple libraries increases the burden to make sure that they are compatible with each other.
Use of multiple libraries and handcrafted HTML / DOM manipulations increases security risks, which would add costs and efforts to additional security audits.
| AngularJS is a well-tested framework that started with the goal of testability.
The built-in components work well together.
AngularJS is used by Google and other enterprise businesses, who regularly contribute security patches to the framework.
A security audit would require far less effort and costs.
|
The arguments against our position are not due to being untruthful or factually incorrect. Rather, the opposing view springs from deftly inverting the very strengths of JavaScript libraries and developer governance into perceived weaknesses, and by morphing “concerns” into major requirements. Our rejoinders came off as feeble and defensive replies - unlike their affirmative appeals for using AngularJS. Their reasoning went like this, “does jQuery support features x, y, and z”? The answer is “no”, leading to their conclusion that AngularJS has more features than jQuery and therefore, AngularJS is the better choice, so we lost the debate.
Libraries supply services or discrete features. Therefore, libraries preserve and enhance governance, by allowing developers to use their existing skills and knowledge with minimal friction to making decisions. On the other hand, frameworks tend to favor conformity and opacity. Consequently, frameworks shift governance away from the developer, creating undue friction that frustrates high governance developers regardless of all the features provided by the framework.
From losing this debate, I had to re-examine and question why Dale’s position had become so acrimonious, and determine how I would properly address opposing criticisms in the future. What are the structures and strategies needed to develop an application using only JavaScript libraries? One could argue: “But isn’t that “reinventing the wheel”? I’d say, this is one way of looking at it. But weren’t the inventors of the steel-belted radial tire “re-inventors” of the wheel? Did that stop them? Is taking the time to preserve and enhance governance worth the effort? I believe that enhancing your knowledge of software architecture, and having the curiosity to understand what is going on underneath the hood makes you a better developer.
Therefore, the motivation for this article is not to debate the technical pros and cons of frameworks like AngularJS. The primary purpose of this article is to enhance developers’ governance. Promoting higher governance means enabling the greater autonomy for developers to exercise authority and decision-making through the leveraging of their skills and experience. Frameworks reduce governance with lock-in paradigms and conformity, while libraries promote higher governance, as they are dependent upon the decision-making skills of the developer to put them to good use.
The Application
The first step in development is the application definition. A good place to find a specification is the TodoMVC website. The website provides a specification of a Todo list manager. There are several examples of TodoMVC projects written using the MV* frameworks, such as AngularJS, Backbone.js, EmberJS, and React. There are also examples of TodoMVC written without any framework: a jQuery version and a VanillaJS version. VanillaJS is an example of the TodoMVC application written without using any JavaScript library or framework.
The Specifications
TodoMVC is a simple Todo task list manager. It allows users to enter each task and it presents a list of tasks. The user can add, remove, edit a task, toggle the completion of a particular task or the entire list of tasks, and remove all of the completed tasks in the list. The application also presents a summary of the number of remaining active tasks, and it filters active and completed tasks. A screenshot of TodoMVC as shown below.
Figure 1: TodoMVC Application
TodoMVC has a pretty straightforward and succinct specification. Critics will argue that this is not a “real world” application. While it is true that TodoMVC is not an enterprise application, our development approach uses real world strategies and architectural patterns to illustrate high-governance development. Our goal is for you, the developer, to retain control of the architecture and development process.
The Strategy
Now that we understand the specifications, the next step is the application architecture. There are two ways we can approach architecture:
1. Satisfy the narrow requirements of this application.
2. Design the application using separation of concerns in an MV* manner.
Since our objective is to address the critiques leveled at using no-framework, the preferred approach is to design our version of TodoMVC as an MV* application. Because we have been criticized for "reinventing the wheel", let's see if we can leverage an existing version of TodoMVC written without a framework.
TodoMVC.com has two versions of the applications written as a no-framework application: jQuery and VanillaJS. The jQuery application uses two additional libraries: Handlebars (for templates handling) and Director (for routing).
Let’s take a look at the jQuery code.
jQuery(function ($) {
'use strict';
Handlebars.registerHelper('eq', function (a, b, options) {
return a === b ? options.fn(this) : options.inverse(this);
});
var ENTER_KEY = 13;
var ESCAPE_KEY = 27;
var util = {
uuid: function () {
var i, random;
var uuid = '';
for (i = 0; i < 32; i++) {
random = Math.random() * 16 | 0;
if (i === 8 || i === 12 || i === 16 || i === 20) {
uuid += '-';
}
uuid += (i === 12
? 4
: (i === 16
? (random & 3 | 8)
: random)).toString(16);
}
return uuid;
},
pluralize: function (count, word) {
return count === 1 ? word : word + 's';
},
store: function (namespace, data) {
if (arguments.length > 1) {
return localStorage.setItem(namespace, JSON.stringify(data));
} else {
var store = localStorage.getItem(namespace);
return (store && JSON.parse(store)) || [];
}
}
};
var App = {
init: function () {
this.todos = util.store('todos-jquery');
this.todoTemplate = Handlebars.compile($('#todo-template').html());
this.footerTemplate = Handlebars.compile($('#footer-template').html());
this.bindEvents();
new Router({
'/:filter': function (filter) {
this.filter = filter;
this.render();
}.bind(this)
}).init('/all');
},
bindEvents: function () {
$('#new-todo').on('keyup', this.create.bind(this));
$('#toggle-all').on('change', this.toggleAll.bind(this));
$('#footer').on('click',
'#clear-completed',
this.destroyCompleted.bind(this));
$('#todo-list')
.on('change', '.toggle', this.toggle.bind(this))
.on('dblclick', 'label', this.edit.bind(this))
.on('keyup', '.edit', this.editKeyup.bind(this))
.on('focusout', '.edit', this.update.bind(this))
.on('click', '.destroy', this.destroy.bind(this));
},
render: function () {
var todos = this.getFilteredTodos();
$('#todo-list').html(this.todoTemplate(todos));
$('#main').toggle(todos.length > 0);
$('#toggle-all').prop('checked', this.getActiveTodos().length === 0);
this.renderFooter();
$('#new-todo').focus();
util.store('todos-jquery', this.todos);
},
renderFooter: function () {
var todoCount = this.todos.length;
var activeTodoCount = this.getActiveTodos().length;
var template = this.footerTemplate({
activeTodoCount: activeTodoCount,
activeTodoWord: util.pluralize(activeTodoCount, 'item'),
completedTodos: todoCount - activeTodoCount,
filter: this.filter
});
$('#footer').toggle(todoCount > 0).html(template);
},
toggleAll: function (e) {
var isChecked = $(e.target).prop('checked');
this.todos.forEach(function (todo) {
todo.completed = isChecked;
});
this.render();
},
getActiveTodos: function () {
return this.todos.filter(function (todo) {
return !todo.completed;
});
},
getCompletedTodos: function () {
return this.todos.filter(function (todo) {
return todo.completed;
});
},
getFilteredTodos: function () {
if (this.filter === 'active') {
return this.getActiveTodos();
}
if (this.filter === 'completed') {
return this.getCompletedTodos();
}
return this.todos;
},
destroyCompleted: function () {
this.todos = this.getActiveTodos();
this.filter = 'all';
this.render();
},
indexFromEl: function (el) {
var id = $(el).closest('li').data('id');
var todos = this.todos;
var i = todos.length;
while (i--) {
if (todos[i].id === id) {
return i;
}
}
},
create: function (e) {
var $input = $(e.target);
var val = $input.val().trim();
if (e.which !== ENTER_KEY || !val) {
return;
}
this.todos.push({
id: util.uuid(),
title: val,
completed: false
});
$input.val('');
this.render();
},
toggle: function (e) {
var i = this.indexFromEl(e.target);
this.todos[i].completed = !this.todos[i].completed;
this.render();
},
edit: function (e) {
var $input = $(e.target).closest('li').addClass('editing').find('.edit');
$input.val($input.val()).focus();
},
editKeyup: function (e) {
if (e.which === ENTER_KEY) {
e.target.blur();
}
if (e.which === ESCAPE_KEY) {
$(e.target).data('abort', true).blur();
}
},
update: function (e) {
var el = e.target;
var $el = $(el);
var val = $el.val().trim();
if (!val) {
this.destroy(e);
return;
}
if ($el.data('abort')) {
$el.data('abort', false);
} else {
this.todos[this.indexFromEl(el)].title = val;
}
this.render();
},
destroy: function (e) {
this.todos.splice(this.indexFromEl(e.target), 1);
this.render();
}
};
App.init();
});
Figure 2: jQuery version of TodoMVC
Well, folks, that’s it, we’re done!. Ok, but not quite. While this code is extremely concise and fulfills the specifications, its structure is tightly coupled to its implementation. Our aim is to have loose coupling and support for an MV* architecture that is reusable and can be extended into future projects.
A closer look at the VanillaJS implementation is more to our liking. The irony here is that despite the no-framework controversy, the code structure of the VanillaJS version is a true MVC application written without using a framework or library.
Therefore, we can utilize VanillaJS and still claim victory. But in order to learn from this exercise, it’s best to take a naïve appraisal of TodoMVC. Let’s assume that we hadn’t encountered the VanillaJS or jQuery versions, yet we still want to adopt no-framework. How do we approach development?
MV* Architecture
MV* represents a suite of architectural patterns that separate the application concerns into discrete components: Model, View, and Controller. The model manages the data, business objects, and data representations that are used by the application. The view presents the data, and it determines the appearance of the application. The controller mediates the interactions between the model and the view. The role of the controller differs between the architectural patterns.
Model-View-Controller (MVC)
In the MVC pattern, the user interacts with the view and the view send user events to the controller, and the controller updates the model. Upon changes to its state, the model notifies the view, and the view queries the model to render the data.
Figure 3: Model-View-Controller
Model-View-Presenter (MVP)
In the MVP pattern, the controller becomes better-known as the presenter as in "presenting" the model’s changes to the view. Unlike the MVC case, all interactions between the model and the view are managed by the presenter. The model communicates changes of its state to the presenter. The presenter then issues commands to the view to present the data. The presenter acts as a mediator to maintain the separation and isolation of the model and the view, and it prohibits any coupling between them.
Figure 4: Model-View-Presenter
Model-View-ViewModel (MVVM)
In the MVVM pattern, the controller becomes the view-model as in "a model of the view". The view-model and the view are connected via data binding. Data binding is a reciprocal event pattern that notifies the view of changes to the view-model and vice versa. The data binding layer hides the details of the binding aspects and event mapping between the view-model and the view.
In MVVM, the view elements and event handlers are declaratively defined using a binding syntax. This syntax is interpreted by the binding layer that automatically "wires up" UI elements, event handlers, and properties of the view-model. This work performed by the data binding layer alleviates developers from having to manually code this effort.
Despite its many advantages, data binding has its drawbacks. Typically, data binding is a "black box", making it inflexible when features are needed that exceed its assumptions and limits. The data binding code is often opaque and proprietary making it difficult to debug and to customize.
Figure 5: Model-View-ViewModel
TodoMVC Architecture
Of the aforementioned MV* architectures, the MVP pattern is the most preferred of the three. My main reasons for this decision are: the transparency of implementation, its intrinsic isolation of the view and the model, and all interactions between the view and the model must flow through the presenter.
The presenter is the "brains" of the application, and it encapsulates the logic that coordinates the interactions. This makes the logical separation of the components much easier to understand, develop, and to maintain.
MVC fell short because the view and the model are not fully isolated from each other. With MVVM, declarative data binding conceals a great amount of the interaction details between the view and view-model. The "behind the scene" activities of data binding reduces governance that could become a real impediment to development and frustrates debugging. Here, the preference for transparency outweighs the benefits of MVVM, even if that preference puts additional burdens on the developer.
TodoMVC Design
Now that we’ve decided to utilize MVP architecture, let's consider the design. TodoMVC uses an object-oriented design. Using an object-oriented design means having to identify the objects and their interactions. Using the MVP architecture identifies three objects: the Controller (Presenter), the View, and the Model. However, other objects are needed to build the application. But let's first obtain an understanding of object-oriented design and object-oriented programming.
Object-orientation
Object-orientation is an approach to building software applications around the concept of objects. An object is an abstraction that contains attributes that represent the characteristics of the object along with methods that provide the object’s behavior. There are four traits that define object-oriented programming (OOP):
-
Abstract Data Types:
An abstract data type represents the classification of a set of attributes and behaviors. When initiated, an object of a particular classification is created. Another way to think of this is that an abstract data type defines the schematic that is used to create an actual object.
-
Encapsulation:
Encapsulation represents the containment of the data and methods inside of a discrete "capsule". The data is hidden from clients and is only accessible through properties and methods.
-
Inheritance:
Inheritance permits the definition of new classes that are derived from reusing the attributes and methods that were defined within a base class. Derived classes are arranged in an "is-a" relationship, whereby these classes are descendent from the base class. Derived classes have the options of either reusing or overriding the base class features.
-
Polymorphism:
Polymorphism is the ability to use the same signatures for methods that have different implementations. For example, in an animal hierarchy, the Animal
base class defines the method speak
. The Dog
and Cat
derive from the Animal
class and inherit the speak
method from Animal
. The speak
method returns the "bark!"
string when called on a Dog
instance. The string "meow!"
is returned when the speak
method is called from a Cat
instance.
In object-oriented programming, polymorphism is achieved through a message protocol. In the aforementioned example, the message protocol is the signature of the speak
method (see Figure 7).
Classical Inheritance in JavaScript
Prior to EcmaScript 6 (ES6), JavaScript did not support class-based or classical inheritance. JavaScript supports prototypical inheritance. A prototype is an object template of properties that are reproduced into new object instances. You can think of the prototype as the "master" copy and new object instance as clones of the master copy. Since JavaScript objects are really dynamic property bags, prototypical inheritance reproduces the property bag of the master object onto the new object.
Classical inheritance is supported in ES6, but it is not fully supported in current browsers. However, classical inheritance is a more refined version of prototypical inheritance. The file inherit.js
emulates classical inheritance by copying base class properties onto its derived class.
(function () {
"use strict";
var derive = function(clazz, base) {
if (clazz.$base)
return;
if (base) {
base.prototype.$init = true;
clazz.prototype = new base();
delete base.prototype.$init;
clazz.prototype.constructor = clazz;
Object.defineProperty(clazz, '$base', {
get: function() {return base;}
});
}
else {
Object.defineProperty(clazz, '$base', {
get: function() {return Object;}
});
}
};
if (!Object.hasOwnProperty('inherit')) {
Object.defineProperty(Object.prototype, 'inherit', {
value: function(clazz, base) {
derive(clazz, base);
for (var property in clazz.prototype) {
if (property !== '$init') {
Object.defineProperty(this, property,
Object.getOwnPropertyDescriptor(clazz.prototype, property));
} else {
delete clazz.prototype[property];
}
}
this.$base = (base) ? clazz.prototype : {};
}
});
}
}());
Figure 6: inherit.js
To perform classical inheritance in JavaScript, the inherit
method creates a derived class from the base class, and it recursively initializes the class hierarchy. If no base class is provided to the inherit
method, then Object
is assumed as the base class. The figure below illustrates the definition of an Animal
class hierarchy and the object-oriented characteristics of encapsulation, inheritance, and polymorphism.
function Animal() {
this.inherit(Animal);
this.speak = function() {
console.log("An animal speaks.");
};
}
function Dog() {
this.inherit(Dog, Animal);
this.speak = function() {
return "bark!";
};
}
function Cat() {
this.inherit(Cat, Animal);
this.speak = function() {
return "meow!";
};
}
var dog = new Dog();
var cat = new Cat();
console.log(dog.speak());
console.log(cat.speak());
Figure 7: Animal class hierarchy using inherit.js
With the understanding of object-oriented programming in place, the next step is to identify the class hierarchy used to construct the TodoMVC application.
TodoMVC Class Hierarchy
The class hierarchy diagram below shows the classes that enable the architectural pattern used by the application. This diagram includes the reusable aforementioned MVP classes: Model
, View
, and, Controller
. Despite its name, the Controller
class for this application, provides the base functionality of the presenter. The Controller
also serves as the mediator between Model
and View
, in order to enforce separation between them.
Classes that are specific to the TodoMVC application operations are TodoView
, TodoModel
, TodoTemplate
, and Todos
. The Todos
class coordinates the interactions between TodoModel
and TodoView
. TodoModel
inherits from the Model
class that contains the Storage
class, which encapsulates the browser's local storage. The Template
class provides a base implementation for processing templates and Map
combines key-value associations with sequential access.
Figure 8: TodoMVC UML with class hierarchy and composition.
Event-Driven Design
At the top of the hierarchy is the Dispatcher
class. Dispatcher
provides the event management of the architecture. An event is an occurrence, that is loosely-coupled to a handler, that performs an action when it is signaled through a message protocol.
The best way to envision an event and message protocol is to think of an actual function invocation. Unlike an event, a function invocation is a tightly coupled occurrence between the message protocol and the handler. For example, a function call doSomething()
tightly binds the occurrence (the doSomething
call), the message protocol (the function name, doSomething
) to the handler (the implementation of the doSomething
function).
In event programming, the event and message protocol are loosely-coupled. An event by the name of "doSomething"
, sends a message to the Dispatcher
, which interprets the message, and then indirectly invokes the doSomething
handler. Using an event-driven approach, the invocation of doSomething
is decoupled and separated into three separate stages as shown below.
Event Stages
| Event-Driven Invocation
| Function Invocation
|
Occurrence
The invocation of the event.
| The triggering of an event.
Example:
dispatch.trigger('doSomething', eventArgs);
| The calling of a method.
Example:
object.doSomething();
|
Message Protocol
Describes the method and the arguments passed to the method.
| The event name and event arguments.
Example:
dispatch.trigger('doSomething', eventArgs);
The message protocol is the event name doSomething including the arguments passed to the implementation attached to the doSomething event name.
| The method signature.
Example:
object.doSomething();
The method signature represents the message protocol of the doSomething method.
|
Implementation
The handler attached to the event.
| The event handler attached to the event name at runtime (late binding). The event handler executes after the event triggers.
Example:
Class myObj {
void doSomething() {
console.log('did something');
}
}
var obj = new myObj();
dispatch.attach('doSomething', obj.doSomething);
The myObj.doSomething method attaches to the doSomething event name.The doSomething method executes when the event named as 'doSomething' triggers.
| The "handler" (method) executes immediately upon method invocation (early binding).
Example:
var obj = new myObj();
obj.doSomething();
The doSomething method executes immediately when invoked (the occurrence).
|
Figure 9: The stages of an event.
A standard function invocation is also known as "early binding" because the binding of the message protocol and handler occurs prior to runtime. The event driven invocation is known as "late-binding" since the message protocol is bound to the function implementation during runtime.
Typically in static languages like C++, Java, and C#, the preferred binding method is early binding. This allows the compiler to verify function calls through their signatures. Naturally, early bound functions provide the best performance. With late-binding, handlers are attached to events during runtime. Late binding impacts performance but offers greater flexibility, due to the decoupling of method invocation and its implementation.
Dispatcher Event Management
The Dispatcher
class is the base class of the Model
, View
, and, Controller
. The Dispatcher
enables these classes to interact in a loosely-coupled manner particularly the loose-coupling between the View
and the Controller
. The View
responds to a user event, processes the user event, and then sends a message to the Controller
. The View
has no knowledge of the Controller
nor does it have any dependency upon the Controller
.
Figure 10: The Dispatcher class.
The Dispatcher
is based on the idea of separation between incoming and outgoing messages. In the case of incoming messages, the client requests the Dispatcher
to perform a service. Client requests are managed as commands.
Figure 11: The Dispatcher separates the concept of incoming (command) and outgoing (event) messages.
A command represents an incoming message sent by a client to the service provider, in order to perform a service. Commands indicate that the client has an awareness of the service provider. On the other hand, an event represents an outgoing message that triggers subscribers of the event. This means that the service provider is unaware of the target of the event. Within the application, the Todos
controller issues commands to TodoView
, in order to perform services. Outgoing messages occur after TodoView
captures and processes user events, followed by publishing events to the Todos
controller. Because TodoView
issues events, it is unaware of the Todos
controller and TodoView
operates independently from the Todos
controller.
In a scripting language like JavaScript, late-binding is commonplace, and it reduces both the coupling and dependencies of key components. To maintain this loose-coupling, the Dispatcher
uses the EventBus
class to perform the management work of events occurrence, event messaging, and event handling using the publisher-subscriber pattern. The diagram below displays the internal structure of the EventBus
.
Figure 12: The eventbus internals.
The EventBus
is composed of publishers and subscribers. The Publisher
class represents a single event message and it manages the subscribers of that particular message. The Subscriber
class represents the event handler. Thus, a single event occurrence gets "published" to many subscribers.
Delegate and Subscriber classes
The Subscriber
class is a bound function wrapper. Prior to the bind
function support in JavaScript, a bound function required an explicit closure definition. A bound function is a function having a preset scope object. If the function's scope object is bound to the this
object then the function essentially become similar to a C# delegate.
The Delegate
class, shown in the figure below, is used by the Subscriber
class. In addition, Delegate
provides an extra level of verification though its scoped object. During unsubscribe, the Publisher
verifies the Subscriber
through both its scoped and callback objects.
var Delegate = (function () {
"use strict";
var create = function(scope, callback) {
function Delegate(obj, func) {
var noop = function() {},
self = obj,
method = func;
this.invoke = function () {
if (self && method) {
return (arguments) ? method.apply(self, arguments) : method.apply(self);
} else {
return noop;
}
};
this.invoke.scope = {
get scope() {
return self;
}
};
this.invoke.callback = {
get callback() {
return method;
}
};
this.invoke.equals = function (delegate) {
return this.scope === delegate.scope && this.callback === delegate.callback;
};
}
return new Delegate(scope, callback).invoke;
};
return {
create: create
};
}());
Figure 13: The Delegate class.
Data Management
TodoMVC persists data through the browser’s localStorage
object, which provides a very easy to use key-value data access. This version of TodoMVC uses one key-value entry of localStorage
in order to store the todo list. The entries of the todo list are persisted as a single JSON string. This strategy was chosen to reduce access to localStorage
and to make persistence a bit more interesting. In order to manage the list items in memory, the Storage
class uses the Map
class.
Map
is a collection that combines associated key-values with sequential access. Therefore, items in the collection are accessible only by a key or an index. The Storage
class uses Map
to access todo items by their unique key, and it retains the entry order of todo items onto the list.
Points of Interest
This article was originally structured as a single piece because to properly convey the no-framework approach, it requires an in-depth explanation. Due to its length, this article had to be segmented into three parts. Part 1, which you've just read, provides the background, the motivation, and an architectural approach for no-framework. Part 2, continues with the implementation of TodoMVC as a no-framework application using the concepts presented in Part 1, and finally in Part 3, armed with a working no-framework application, rebut the arguments presented against the no-framework approach.
History
11 Jun 2016 Explanation of the background, motivation and architectural approach to no-framework.
18 Jun 2016 Fixed table format errors.