Vertebrae framework

built with Backbone.js and RequireJS using AMD

Vertebrae Front-end Framework Built With Backbone.js and RequireJS Using AMD

Vertebrae provides AMD structure and additional objects for extending Backbone.js as an application framework.

The plan: build an application, mostly with open source libraries

Following a review of many MV* front-end frameworks/libraries, such as Ember, JavaScriptMVC, Spine, Knockout, and Backbone.js our team decided to begin building a framework with Backbone.js, RequireJS, Underscore.js, jQuery, Mustache.js with code organized in packages of (AMD) modules, see previous post: Optimize and Build a Backbone.js JavaScript application with Require.JS using Packages

We needed to add some logic on top of the chosen stack of libraries

The libraries we selected provide: module loading, dependency management, application structure (for models, collections, views and routes), asynchronous interactions with API, various utilities and objects to manage asynchronous behaviors, e.g. (Promises) Deferreds, Callbacks. The remaining logic needed to complete the application include:

  • An object (collection) to manage state(s) in the single-page application;
  • A layout manager to present, arrange/transition and clear views, and
  • Controllers which respond to routes, get/set application state, and hand off views and models/collections to layout manager.

The project goals include:

  • Modules for code organization (a module should have a single responsibility)
  • MVC structure for separation of concerns
  • Dependency management using a script loader
  • Build tools for optimization in packages which should support various experiences in the app
  • Well documented and actively used open source libraries
  • Framework code should have unit tests, build framework by writing unit tests.
  • Solutions for managing asynchronous behaviors, e.g. Promises

We are using: RequireJS, Underscore.js, jQuery, Backbone.js, Mustache.js; and for BDD… Jasmine, Sinon, jQuery-Jasmine.

Solutions

Application state manager

The application manager stores data in memory (collection) and also persists data in browser storage to provide a resource for storing and retrieving data/metadata. The application states collection also provides data (state) to reconstruct a page (layout view) based on previous interactions (e.g. selected tab, active views, etc.). The application state manager provides a strategy for resources to retrieve state for a common interface for localStorage, sessionStorage, or a cookie; also sets an expires property.

Also, data used to bootstrap a page can be stored as well, e.g. user/visitor information (items in cart, etc.), data that is reused for many pages, interactions on a page that should be recalled when a user revisits the page.

Layout manager

The layout manager has one or many views as well as document (DOM) destinations for each (rendered) view. A page may transition between views, so the layout manager keeps track of view states, e.g. rendered, not-rendered, displayed, not-displayed. The layout manager can load and render (detached) views that a user/visitor is very likely to request, e.g. tab changes on page. The transition between view states is managed by this object. An entire layout may be cleared so that view objects and their bindings are removed, preparing these objects for garbage collection (preventing memory leaks). The layout manager also communicates view state with controller(s).

Controller

A controller object is called by a route handler function, is responsible for getting relevant state (application models), and does the work to construct a page (layout); also responsible for setting state when routes change. The controller passes dependent data (models/collections) and constructed view objects for a requested page to the layout manager. As a side-effect the use of controllers prevents the routes object from becoming bloated and tangled. A route should map to a controller which then kicks off the page view, keeping the route handling functions lean.

Behavior Driven Development -

We made the decision to develop the framework using behavior driven development. Jamsine is the test framework we selected, below are the specs that were used to discover the solutions for our need to have the additional logic we wanted to add on top of the open source stack of libraries…

Specs

Application state manager specs
(an model object to manage state within the single-page application)

should use options for persistent storage of JSON data
should reference stored data with string name, e.g. route name
should have expiration for data objects that are persisted in localStorage
should store (in memory) reference to model/collection objects for reuse
should be a singleton object
should provide strategy for data retrieval (memory/storage/api)
should store view state of page layouts

Layout manager specs
(presents, arranges, transitions and clears views)

should use ‘destination’ property for location on dom to show each view should keep track of current state for view objects, e.g. not-rendered, shown, hidden should lazy/load or render hidden (detached) views within a layout should have show and close methods to render and display a view should manage transition between views within a layout scheme should close (destroy) managed views/bindings within a layout (e.g. on transition to new layout) should have access to view’s deferred object (returned on render) for asynchronous display should have option to display layout when all views’ deferreds are resolved or as ready

Controller specs
(gets/sets application state, and delegates work to a layout manager object, used within route handlers)

should get data from application state manager object
should initialize views/models with relevant data received from application state manager
should call layout manager with arguments including relevant views/data
should send data to manager when route changes to store view state or dependent data
should receive data from view objects which publish change in view state

Views:

We studied many posts on the topic of Backbone.js and came up with a set of views that will support all the work we needed and provide some patterns to reuse in our application…

BaseView, CollectionView, SectionView, LayoutView (Manages Sections)

All the views extend the BaseView which extends the Backbone.View object.

Base View

A view object to construct a standard view with common properties and utilties The base view extends Backbone.View adding methods for resolving deferreds, rendering, decorating data just in time for rendering, adding child views to form a composite of views under one view object, add a destroy method.

Collection View

Manages rendering many views with a collection. The CollectionView extends BaseView and is intended for rendering a collection. A item view is required for rendering within each iteration over the models. This was a great source for the CollectionView: http://liquidmedia.ca/blog/2011/02/lib-js-part-3/

Section View

View object to track view’s state ‘not-rendered’, ‘rendered’, ‘not-displayed’, ‘displayed’; can be mixed in to another view type, e.g. CollectionView.

A section view is the required view object for a layout view which expects views to track their own state. This view may be extended as need. to use in a layout, perhaps adding the Section prototype properties to another view.

Layout Manager View

Presents, arranges, transitions and clears views

The layout manager has one or many views as well as document (DOM) destinations for each (rendered) view. A page may transition between many views, so the layout manager keeps track of view states, e.g. ‘not-rendered’, ‘rendered’, ‘not-displayed’, ‘displayed’.

The layout manager can lazy load and render (detached) views that a member is very likely to request, e.g. tab changes on events page. The transition between view states is managed by this object. An entire layout may be cleared so that view objects and their bindings are removed, preparing these objects for garbage collection (preventing memory leaks). The layout manager also communicates view state with controller(s).

Models:

BaseModel, ApplicationState

Application State Model

A model object to manage state within the single-page application Attributes: {String} name, {Object} data, {String} storage, {Date} expires

All the models extend the BaseModel which extends the Backbone.Model object.

Collections

BaseCollection, ApplicationStates

Application State Collection

A collection object to reference various states in the application.

The application manager stores data in memory and also persists data in browser storage to provide a resource for common data/metadata. Also provides data (state) to reconstruct the page views based on previous interactions (e.g. selected tab, applied filters). The application state manager provides a strategy for resources to retrieve state.

All the collections extend the BaseCollection which extends the Backbone.Collection object.

Syncs:

syncFactory, application-state, storageFactory

We have a sync to use localStorage, sessionStorage, or a cookie using the same interface. The application state manager (collection) uses a specific sync object for the browser storage options.

Utils:

ajax-options, docCookies, debug, storage, shims, lib [checkType, duckTypeCheck, Channel (pub/sub), loadCss, formatCase, formatMoney]

We put any of our utilities into a library module.

Controller

A controller object should called within a route handler function, and may be responsible for getting relevant state (application models) to generate a page (layout), (also responsible for setting state when routes change). The controller passes dependent data (models/collections) and constructed view objects for a requested page to the layout manager. As a side-effect the use of controllers prevents the routes object from becoming bloated and tangled. A route should map to a controller which then kicks off the page view, keeping the route handling functions lean.

Facade

Vendor libraries and specific methods used in the framework are required in the facade, and referenced from the facade module in the views, models, collections, lib and other objects in the framework.

AMD - Asynchronous Module Definition

The the examples below show how we are using facade module as a dependency. RequireJS has various options for the syntax you can use to manage dependencies, below are a couple we use:

define(['facade','utils'], function (facade, utils) {

    var ModuleName,
        // References to objects nested in dependencies
        Backbone = facade.Backbone,
        $ = facade.$,
        _ = facade._,
        lib = utils.lib;

    ModuleName = DO SOMETHING HERE

    return ModuleName;
});


define(['require','facade','utils'], function (require) {

    var ModuleName,
        // Dependencies
        facade = require('facade'),
        utils = require('utils'),
        // References to objects nested in dependencies
        Backbone = facade.Backbone,
        $ = facade.$,
        _ = facade._,
        lib = utils.lib;

    ModuleName = DO SOMETHING HERE

    return ModuleName;
});

References:
* AMD spec * RequireJS why AMD * RequireJS AMD

Docs

View docs on the demo site, hosted on Heroku at:

http://vertebrae-framework.herokuapp.com/docs/
http://vertebrae-framework.herokuapp.com/models/docs/
http://vertebrae-framework.herokuapp.com/collections/docs/
http://vertebrae-framework.herokuapp.com/syncs/docs/
http://vertebrae-framework.herokuapp.com/utils/docs/
http://vertebrae-framework.herokuapp.com/views/docs/

Hello World Code Demo Using Layout Manager and Controller

Overview of Hello World “Layout”

Code demonstration of adding a route, route handler function in App, new “hello” package with a template and view. The package controller file hello.js extends the Controller.prototype and is based on a (template) copy of the Controller.prototype in src/controller.js. The WelcomeSectionView prototype extends the SectionView prototype (class) and requries both name and destination properties when instantiated. The application.js method ‘showHello’ is mapped to the route ‘/hello/:name’ and the showHello method instantiates a controller object

Files edited in the application:

src/application.js  
src/main.js  

Files added as a new package:

src/packages/hello.js  [returns: HelloController]  
src/packages/hello/models/welcome.js  
src/packages/hello/templates/layout.html  [HTML used by layout, has section element]  
src/packages/hello/templates/welcome.html  
src/packages/hello/views/welcome.js  [returns: WelcomeSectionView, with article element] 
src/packages/hello/welcome.css

New Route added in src/application.js

1
'hello/:name': 'showHello'

New Package added in src/main.js

1
2
// ** Packages **
'hello'        : HL.prependBuild('/packages/hello'),

Add dependency to application.js

1
2
3
define([ /*... ,*/ "hello" ], function ( /*... ,*/ HelloController ) {
    // BTW this is the AMD module format with "hello" file as dependecy  
});

Add Method for new ‘/hello/:name’ route handler

1
2
3
4
5
6
7
showHello: function (name) {
    controller = new HelloController({
        "params": { "name": name },
        "route": "/hello" + name,
        "appStates" : this.states
    });
},

The parameters hash is added as an option above for the controller object to deal with.

(layout.html) download
1
<section id="welcome"></section>
(hello.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// Hello Controller
// ---------------
// module as controller for 'hello' package
// Returns {HelloController} constructor

define([
        "vendor",
        "controller",
        "models",
        "views",
        "text!hello/templates/layout.html",
        "hello/models/welcome",
        "hello/views/welcome",
        "utils"
        ],
function (
        vendor,
        Controller,
        models,
        views,
        layoutTemplate,
        WelcomeModel,
        WelcomeSectionView,
        utils
        ) {

    var HelloController,
        LayoutView = views.LayoutView,
        BaseModel = models.BaseModel,
        $ = vendor.$,
        _ = vendor._,
        debug = utils.debug,
        Channel = utils.baselib.Channel,
        cssArr = [
            HL.prependBuild("/packages/hello/welcome.css")
        ];

    HelloController = Controller.extend(    {

            initialize: function (options) {
                Channel('load:css').publish(cssArr);

                _.bindAll(this);

                this.handleOptions(options);
                this.handleDeferreds();

                return this;
            },

            setupSections: function () {
                var welcomeView, welcomeModel;

                welcomeModel = new WelcomeModel(this.params);

                welcomeView = new WelcomeSectionView({
                    model: welcomeModel,
                    name: "Welcome",
                    destination: '#welcome'
                });

                debug.log("hello controller setup welcomeView");
                this.sections["Welcome"] = welcomeView;
                this.meta.activeViews.push("Welcome");
            },

            setupScheme: function () {
                this.scheme.push(this.sections[this.meta.activeViews[0]]);
            },

            setupLayout: function () {
                var helloLayout;

                helloLayout = new LayoutView({
                    scheme: this.scheme,
                    destination: "#content",
                    // require a html page layout template with text! prefix
                    template: layoutTemplate,
                    displayWhen: "ready"
                });
                this.layout = helloLayout;

                return this.layout;
            },

            handleDeferreds: function () {
                var controller = this;

                $.when(
                    null // or deferred objects, comma separated e.g. this.eventData.request 
                ).then(function () {
                    controller.setupSections();
                    controller.setupScheme();
                    controller.setupLayout().render();
                });
            },

            handleOptions: function (options) {
                if (options.params) {
                    this.params = options.params;
                }
                Controller.prototype.handleOptions(options);
            }

        });

    return HelloController;
});
(welcome.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Welcome Model
// -------------

// Requires define  
// Return {WelcomeModel} model constructor object  

define(['models'], function (models) {

    var WelcomeModel,
        BaseModel = models.BaseModel;

    WelcomeModel = BaseModel.extend({

        defaults: {
            name: null
        }

    });

    return WelcomeModel;
});
(welcome.html) download
1
<p>Hello {{name}}!</p>
(welcome.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Welcome Section View
// ------------------

// Package Hello
// Requires define
// Returns {WelcomeSectionView} constructor

// Contrete prototype extends SectionView.prototype (class) to be used in a LayoutView.

define([
        'views',
        'hello/models/welcome',
        'text!hello/templates/welcome.html'
        ],
function (
        views,
        WelcomeModel,
        welcomeTemplate
        ) {

    var WelcomeSectionView,
        SectionView = views.SectionView;

    WelcomeSectionView = SectionView.extend({

        tagName: 'article',

        className: 'welcome',

        model: WelcomeModel,

        template: welcomeTemplate // "Hello {{name}}!"

    });

    return WelcomeSectionView;
});
(welcome.css) download
1
2
3
4
5
6
7
8
9
10
11
/* Welcome styles */

#welcome {
    margin: 0 50%;
    padding: 5em 0;
    font: normal 2em/1.5em monospace;
}
.article p {
    color: blue;
    text-align: center;
}

Part 2: Get JSON data for content using AJAX

To get the ‘About’ section data a fixture (JSON file) was added in the test directory.

(101) download
1
2
3
4
5
6
7
8
9
{
    "_links": {
        "self": "/test/fixtures/hello/101",
        "shop": "http://www.hautelook.com/"
    },
    "title": "About HauteLook",
    "content": "Welcome to HauteLook, where you will discover thousands of the top fashion and lifestyle brands at amazing savings. Each day at 8 AM Pacific, shop new sale events featuring the best names in women's and men's fashion and accessories, beauty, kids' apparel and toys, and home décor at up to 75% off. Membership is free and everyone is welcome!",
    "callToAction": "To start shopping, go to: <a href=\"www.hautelook.com\">www.hautelook.com</a>"
}

application.js updated with…

1
2
3
4
5
routes: {
    'hello': 'showHello',
    'hello/': 'showHello',
    'hello/:name': 'showHello'
},
1
2
3
4
5
6
7
8
showHello: function (name) {
    controller = new HelloController({
        "params": { "name": name },
        "route": (name) ? "/hello" + name : "/hello",
        "appStates" : this.states,
        "useFixtures" : true
    });
},

Some files have changed…

src/packages/hello.js  
src/packages/hello/models/about.js  
src/packages/hello/templates/about.html  
src/packages/hello/templates/layout.html  
src/packages/hello/templates/welcome.html  
src/packages/hello/views/about.js  
src/packages/hello/views/welcome.js  
src/packages/hello/welcome.css

See the source code below and see how the new “About” section view is added in addition to the simple hello name view created by the welcome view.

(layout.html) download
1
2
3
4
<div id="welcome">
    <section id="greeting"></section>
    <section id="about"></section>
</div>
(hello2.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
// Hello Controller
// ---------------
// module as controller for 'hello' package
// Returns {HelloController} constructor

define([
        "vendor",
        "controller",
        "models",
        "views",
        "text!hello/templates/layout.html",
        "hello/models/welcome",
        "hello/views/welcome",
        "hello/models/about",
        "hello/views/about",
        "utils"
        ],
function (
        vendor,
        Controller,
        models,
        views,
        layoutTemplate,
        WelcomeModel,
        WelcomeSectionView,
        AboutModel,
        AboutSectionView,
        utils
        ) {

    var HelloController,
        LayoutView = views.LayoutView,
        BaseModel = models.BaseModel,
        $ = vendor.$,
        _ = vendor._,
        debug = utils.debug,
        Channel = utils.baselib.Channel,
        cssArr = [
            HL.prependBuild("/packages/hello/welcome.css")
        ];

    HelloController = Controller.extend(    {

            initialize: function (options) {
                Channel('load:css').publish(cssArr);

                _.bindAll(this);

                this.handleOptions(options);
                this.setupSections();
                this.handleDeferreds();

                return this;
            },

            setupWelcomeSection: function () {
                var welcomeView, welcomeModel;

                welcomeModel = new WelcomeModel(this.params);

                welcomeView = new WelcomeSectionView({
                    model: welcomeModel,
                    name: "Welcome",
                    destination: '#greeting'
                });

                debug.log("hello controller setup welcomeView");
                this.sections["Welcome"] = welcomeView;
            },

            setupAboutSection: function () {
                var aboutView, aboutModel,
                    webservice = this.webServices.hello;

                aboutModel = new AboutModel(
                    {id: 101}, // attributes
                    {urlRoot: webservice} // options
                );
                aboutModel.request = aboutModel.fetch();

                aboutView = new AboutSectionView({
                    model: aboutModel,
                    name: "About",
                    destination: '#about'
                });

                debug.log("hello controller setup aboutView");
                this.sections["About"] = aboutView;
            },

            setupSections: function () {
                var params = this.params;

                if (params && params.name && _.isString(params.name)) {
                    this.setupWelcomeSection();
                    this.meta.activeViews.push("Welcome");
                }
                this.setupAboutSection();
                this.meta.activeViews.push("About");
            },

            setupScheme: function () {
                var i, params = this.params;

                for (i = 0; i < this.meta.activeViews.length; i++) {
                    this.scheme.push(this.sections[this.meta.activeViews[i]]);
                };
            },

            setupLayout: function () {
                var helloLayout;

                helloLayout = new LayoutView({
                    scheme: this.scheme,
                    destination: "#content",
                    // require a html page layout template with text! prefix
                    template: layoutTemplate,
                    displayWhen: "ready"
                });
                this.layout = helloLayout;

                return this.layout;
            },

            handleDeferreds: function () {
                var controller = this;
                    aboutRequest = (this.sections["About"]) ? this.sections["About"].model.request : null;

                $.when(aboutRequest).then(function () {
                    controller.setupScheme();
                    controller.setupLayout().render();
                });
            },

            handleOptions: function (options) {
                if (options.params) {
                    this.params = options.params;
                }
                Controller.prototype.handleOptions(options);
            }

        });

    return HelloController;
});
(welcome.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Welcome Model
// -------------

// Requires define  
// Return {WelcomeModel} model constructor object  

define(['models'], function (models) {

    var WelcomeModel,
        BaseModel = models.BaseModel;

    WelcomeModel = BaseModel.extend({

        defaults: {
            name: null
        }

    });

    return WelcomeModel;
});
(welcome.html) download
1
<code>Hello {{name}}!</code>
(welcome.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Welcome Section View
// ------------------

// Package Hello
// Requires define
// Returns {WelcomeSectionView} constructor

// Contrete prototype extends SectionView.prototype (class) to be used in a LayoutView.

define([
        'views',
        'hello/models/welcome',
        'text!hello/templates/welcome.html'
        ],
function (
        views,
        WelcomeModel,
        welcomeTemplate
        ) {

    var WelcomeSectionView,
        SectionView = views.SectionView;

    WelcomeSectionView = SectionView.extend({

        tagName: 'article',

        className: 'greeting',

        model: WelcomeModel,

        template: welcomeTemplate // "Hello {{name}}!"

    });

    return WelcomeSectionView;
});
(about.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Welcome Model
// -------------

// Requires define  
// Return {AboutModel} model constructor object  

define(['models'], function (models) {

    var AboutModel,
        BaseModel = models.BaseModel;

    AboutModel = BaseModel.extend({

        initialize: function (attributes, options) {
            BaseModel.prototype.initialize.call(this, attributes, options);
        },

        defaults: {
            _links: {
                "self": "/test/fixtures/hello/101",
                "shop": null
            },
            title: null,
            callToAction : null
        }

    });

    return AboutModel;
});
(about.html) download
1
2
3
4
5
<h1>{{title}}</h1>
<code>{{content}}</code>
<p>
    <strong>{{{callToAction}}}</strong>
</p>
(about.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// About '101' Section View
// ------------------

// Package Hello
// Requires define
// Returns {AboutSectionView} constructor

// Contrete prototype extends SectionView.prototype (class) to be used in a LayoutView.

define([
        'views',
        'hello/models/about',
        'text!hello/templates/about.html'
        ],
function (
        views,
        AboutModel,
        aboutTemplate
        ) {

    var AboutSectionView,
        SectionView = views.SectionView;

    AboutSectionView = SectionView.extend({

        tagName: 'article',

        className: 'about',

        model: AboutModel,

        template: aboutTemplate

    });

    return AboutSectionView;
});
(welcome.css) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* Welcome styles */

#welcome {
    margin: 0 20%;
    padding: 5em 0;
    font: normal 2em/1.5em monospace;
}
#welcome code {
    margin: 0 0 1em;
}
#welcome p {
    color: blue;
    text-align: center;
}

Optimize and Build a Backbone.js JavaScript Application With Require.JS Using Packages

When a JavaScript application is too complex or large to build in a single file, grouping the application’s components into packages allows for script dependencies to download in parallel, and facilitates only loading packaged and other modular code as the site experience requires the specific set of dependencies.

Require.JS, the (JavaScript) module loading library, has an optimizer to build a JavaScript-based application and provides various options. A build profile is the recipe for your build, much like a build.xml file is used to build a project with ANT. The benefit of building with r.js not only results in speedy script loading with minified code, but also provides a way to package components of your application.

In a complex application, organizing code into packages is an attractive build strategy. The build profile in this article is based on an test application currently under development (files list below). The application framework is built with open source libraries. The main objective in this build profile is to optimize an application developed with Backbone.js using modular code, following the Asynchronous Module Definition (AMD) format. AMD and Require.JS provide the structure for writing modular code with dependencies. Backbone.js provides the code organization for developing models, views and collections and also interactions with a RESTful API.

Backbone.js CollectionView to Utilize a Collection Rather Than a Model

Backbone.js does not provide a view or controller that specifically manages a collection. Backbone view objects are coupled with a model and have render methods for presented the model data using an HTML template. However there are many instanced where a Backbone collection objects (has many models) needs to be presented or managed with a single view object. Thus the need for a CollectionView constructor that implements an interface to manage many child views which render each model represented in the collection. A collection view object that generates view for each model preserves the core Backbone behavior (link is to documented source, search for ‘change’) of firing change events on a model and a view that renders the changed data in response to the change event that was triggered by specific models.

Liquid Media has posted tutorials on Backbone in a 3-part series 1, 2, 3; and the third article demonstrates the use of a Collection View constructor which does what is described above.

Backbone.js and Mustache.js: Small Views and Templates

Short and concise HTML templates that represent repeatable blocks or isolated components of a page are easier to manage and easier for other developers to edit than longer templates which encompass larger portions of a page. A view object with the responsibility of rendering and managing multiple child views is used to organize the components of a page.

Consider navigation elements organized in lists under top level list items…

Each top level item likely uses the same child list to present it’s dropdowns. Rather then use one large template and one large collection of models utilizing smaller templates as components of the view remove hardcoding the hierarchy into both the model and the template but rather move the hierarchy to the view objects used to build the page. When the structure of the child lists needs to go through the change control process for added or removed presentation or a change in the hierarchy as directed by the business, the change is simple perhaps changed one portion of a template and the order of an array which dictates the hierarchy via enumerating. Also the business may dictate that the order of the menu follows various orders depending on visitor properties (e.g. member object either male or female). Then the menu’s components display order can be dictated programmatically, utilizing the small views and templates.

The best practice or goal emphasized above with respect to templates and views is KISS and DRY. As long as the implementation does not become overly complex and difficult to grok, keep the template code DRY, otherwise KISS principle overrides the need to have template code that does not repeat itself.

Base Classes With Custom Library Functions Added to Backbone.js Constructors

Authoring Code with Backbone.js with extendability in mind

The main objective in choosing the Backbone.js library for our frontend framework is to author code in an organized and repeatable manner, building an application “the Backbone way”. The benefits are that the community has contributed documenation, blogs, code examples, tutorials, videos and books so that building a JavaScript application with our framework should be straight forward (not too much magic under cover).

However when building both the mobile and desktop applications or in the future we need added behavior recuired to provide a solution for the business requirements/needs of the Web application. Hence the need for an (abstract class) object as the base constructor for our framework. Each base object extends the appropriate Backbone constructor method: Backbone.Model, Backbone.View, Backbone.Collection and Backbone.Router.

Backbone Views Using Mustache Templates

Using Mustache.js

Mustache is a template library based on logic-less templates and the JavaScript implementation can be executed on the client side. The basic usage takes two components:

  1. An HTML template with variables embedded inside double curly braces, e.g. <p>{{myVar}}</p>
  2. A JSON object with data containing the named object referenced in the template e.g. { “myVar” : “Sting value here” }.

The Mustache.js api provides a method Mustache.to_html() which compiles the template code with the JSON object to produce a string to insert into the DOM. For example: rendered = Mustache.to_html(view.template, view.model.toJSON());

Simple template used to render a list item

This example will be used by a Backbone View object’s render method which will be used within a CollectionView object with the responsibility of building views from a Backbone collection object

(event.html) download
1
<a href="/event/{{event_code}}" data-sort="{{sort_order}}">{{title}}</a>

JSON Data: Event object

The data below includes the variables (event_code, sort_order, title) referenced in the the template and other data not utilized in the template.

(event-object.json) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
    "event_id": "12366",
    "event_code": "12366stellaandjamiewc",
    "start_date": "2011-12-28T08:00:00-08:00",
    "end_date": "2011-12-30T08:00:00-08:00",
    "image_url": "/assets/12366stellaandjamiewc/event-small.jpg",
    "info": "/assets/12366stellaandjamiewc/info.txt",
    "video": "/assets/12366stellaandjamiewc/video.mp4",
    "title": "Stella & jamie",
    "tagline": "Sequins and detailing that dress things up",
    "event_types": [
        {
            "name": "Women"
        }
    ],
    "meta": {
        "msa": [],
        "status": "Active",
        "private_event": false,
        "exclusive": false,
        "preview": false,
        "nested": {
            "parents": [],
            "children": [],
            "siblings": []
        }
    },
    "description": "Sequins and detailing that dress things up.",
    "coa": "womens_apparel",
    "sort_order": "2",
    "category": "Accessories",
    "event_display_format": "Regular"
}

Backbone View in a AMD module utilizing Mustache within it’s render() method

The module requires a text/html file by including a string in the dependencies array : 'text!packages/header/templates/event.html' and assigns the text string to the variable listed in the anonymous function’s arguments named: eventTemplate. The JSON data is injected when this view constructor is initialized by calling the construction function with the “new” operator and passing in the JSON object as the model data, e.g. "model" : new EventView(model),

The code rendered = Mustache.to_html(view.template, view.model.toJSON()); has a (string) template as the first argument and the second argument is the JSON data object. The result of this to_html method is assigned to the variable rendered. Then the view instance’s “render()” method inserts the rendered html into the DOM reference by the Backbone View object’s “el” property… $(view.el).html(rendered);

(event.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// event view 
// ----------
// @requires define

define(['jquery',
        'underscore',
        'backbone',
        'views/base',
        'text!packages/header/templates/event.html',
        'mustache',
        'utils/debug'],
function ($, _, Backbone, BaseView, eventTemplate, Mustache, debug) {

    var EventView;

    EventView = BaseView.extend({

        initialize: function () {
            this.template = eventTemplate;
            debug.log('EventView init');
        },
        render: function () {
            var view = this, rendered;

            rendered = Mustache.to_html(view.template, view.model.toJSON());
            $(view.el).html(rendered);
            debug.log('EventView rendered');
            return view;
        }
    });

    return EventView;
});

Summary

Mustache.js is the template library selected primarily because of the fact that the library is “logic-less” and also is a popular choice since Mustache is implemmented in a wide variety of languages. The fact that no logic is executed in rendering a Mustache template insures that bugs are highly unlikely to be caused due to some logic failing within the operation of executing the template method to render an object as HTML. Logic can be added to the Mustache template within the JavaScript code (e.g. a module for creating a View constructor) which allows for unit testing and debugging utilizing a breakpoint in the code. See the Mustache.js documentation for using conditions, enumerable objects, partials an other Mustache interface functions to for authoring templates with JavaScript, HTML and JSON.

File Organization: Building a Framework Using RequireJS

When a JavaScript application is to complex or large to build in a single file, grouping the application’s components into packages allows for script dependencies to download in parallel; and facilitates only loading packaged and other modular code as the site experience requires the specific set of dependencies.

AMD and Require.JS provide the structure for writing modular code with dependencies. Backbone.js provides the code organization for developing models, views and collections and also interactions with a RESTful API. Below is an outline of the Web application’s file organization

File organization

Assume the following directories and file organization, with app.build.js as the build profile (a sibling to both source and release directories). Note that the files in the list below named section can be any component of the application, e.g. header, login)

This structure groups common (or site-wide) code of (core) models, views, collections which are extended from a base.js constructor. These base constructors extend the appropriate backbone method, e.g. Backbone.Model.

The packages directory organizes code by section / responsibility, e.g. cart, checkout, etc. Notice that within the example header package the directory structure is similar to the app root directory file structure.

A package (of modularized code) has dependencies from the common libraries in your application and also has specific code for the packages execution alone; other packages should not require another packages dependencies.

A utils directory has shims, helpers, and common library code to support the application.

A syncs directory to define persistence with your RESTful api and/or localStorage.

The vendor libraries folder will not be built, there is no need to do so.

Dependencies Managed With Require() and Define()

Modular Code Dependencies

RequireJS and AMD (Asynchronous Module Definition) provide a pattern for authoring modular code and also managing dependencies without complex namespacing or even adding any properties to the (head) window object. When we write modules in JavaScript, we want to handle the reuse of code intelligently and have the option to build an entire application into a single script or load modules as they are required to execute the business requirements of the application.

Writing AMD modules with RequireJS

Note - This section “Writing AMD modules with RequireJS” is an excerpt from Backbone Fundamentals

The overall goal for the AMD format is to provide a solution for modular JavaScript that developers can use today. The two key concepts you need to be aware of when using it with a script-loader are a define() method for facilitating module definition and a require() method for handling dependency loading. define() is used to define named or unnamed modules based on the proposal using the following signature: