Click here to Skip to main content
15,878,959 members
Articles / Web Development / HTML

The High-Governance of No-Framework - Part 2

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
18 Jun 2016CPOL11 min read 9.7K   61   6  
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.

Image 1
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.

JavaScript
/*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();
        })
    };
}
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.

Image 2
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.

Image 3
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.

JavaScript
/*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);
    };
}
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.

Image 4
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.

JavaScript
/*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);
    };
}
Figure 19: Template.js.

TodoTemplate supports three templating strategies:

  1. Templates defined in an object.

  2. Templates defined in HTML

  3. 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.

JavaScript
/*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');
    };
}
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.

Image 5
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:

  1. an empty object as a placeholder for DOM elements (dom)

  2. an instance of TodoTemplate

  3. a container object for the view commands (viewCommands)

  4. 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.

JavaScript
/*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);
    };
}
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.

Image 6
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.

JavaScript
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.
    });
}
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.

JavaScript
/*
 * 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);
    };
}
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.

JavaScript
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); 
    };
}
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.

JavaScript
/**
 * 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);
    };
}
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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Chris Solutions
New Zealand New Zealand
Chris Waldron is a Solutions Architect, Expert .NET and Full Stack cross-platform Developer and Lead, with decades of top flight international experience at the world’s leading computer software companies including IBM, Getty Images, and Dell Computer Corporation, with over a decade of development experience on signature projects at Microsoft Corporation in Redmond, Washington USA.

Global experience includes product development for vertical markets, web services, and mobile applications, with the delivery of cost-efficient IT tools and solutions to client companies. Expertise also includes providing Lead Development, Architecture, and Project Management for the creation of several successful start-ups, web-based marketing services, and e-commerce businesses.

As the Senior .NET Developer/Architect at Booktrack Ltd in New Zealand, Chris was 1 of only 3 developers on the small team at this Auckland startup. As a result, Booktrack was the Winner - NZ High Tech Awards 2012, winning the top award in both categories: "Most Innovative" Mobile Technology, and Software Product development for the year.

Comments and Discussions

 
-- There are no messages in this forum --