Vertebrae framework

built with Backbone.js and RequireJS using AMD

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.

For example jQuery provides a “Deferred” constructor function ”jQuery.Deferred()” based on the CommonJS Promises/A.

“introduces several enhancements to the way callbacks are managed and invoked. In particular, jQuery.Deferred() provides flexible ways to provide multiple callbacks, and these callbacks can be invoked regardless of whether the original callback dispatch has already occurred.” - jQuery API: Deferred Object

Base constructors which extend Backbone.js constructors

Base Model

This object extends the Backbone.Model constructor adding methods for calling __super__ and insuring that only a instance of a Backbone.Model’s initialization methods is called when calling this.__super__.initialize.call(this); within an instance. Backbone constructors and be further extend and utilize JavaScript prototypal inheritance so this __super__ method is sugar for this.constructor.prototype.initialize.call(this, attributes, options) but insuring that null or undefined is not called but rather the initialize method of an ancestor of the Backbone instance object not a native JavaScript object’s constructor which does not have an initialize function. Also the base object has a property named “deferred” which will be an instance of the jQuery.Deferred object when the constructor is initialized. A deferred object is vary useful to modules that require a instance of the Backbone model that fetches data from our RESTful api. Or when extending/decorating a constructor with additional methods and adding more “done” callbacks

(base.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
// Base Model  
// -------------

// Requires `define`  
// Return {BaseModel} object as constructor

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

    var BaseModel,
        Backbone = facade.Backbone,
        $ = facade.$,
        _ = facade._,
        lib = utils.baselib,
        ajaxOptions = utils.ajaxOptions,
        debug = utils.debug;

    // Constructor `{BaseModel}` extends Backbone.Model.prototype
    // object literal argument to extend is the prototype for the BaseModel constructor
    BaseModel = Backbone.Model.extend({

        // Param {Object} `attributes` set on model when creating an instance  
        // Param {Object} `options`  
        initialize: function (attributes, options) {
            // debug.log("BaseModel init called");
            if (options) {
                this.options = options;
                this.setOptions();
            }
            this.deferred = new $.Deferred();
            // Backbone.Model.prototype.initialize.call(this, arguments);
        },

        // **Property:** `request` - assign fetch return value to this.request property, 
        // fetch returns (jQuery) ajax promise object
        request: null,

        // **Method:** `fetch`
        // Wrap Backbone.Model.prototype.fetch with support for deferreds
        fetch: function (options) {
            options = options || {};
            if (!options.success) {
                options.success = this.fetchSuccess;
            }
            if (!options.error) {
                options.error = this.fetchError;
            }
            _.extend(options, ajaxOptions);
            return this.request = Backbone.Model.prototype.fetch.call(this, options);
        },

        // Default success and error handlers used with this.fetch() ...

        // **Method:** `fetchSuccess` - resolve the deferred here in success
        fetchSuccess: function (model, response) {
            if (model.deferred) {
                if (!model.request) {
                    model.request = model.deferred.promise();
                }
                model.deferred.resolve();
            }
            debug.log(response);
        },

        // **Method:** `fetchError` - log response on error
        fetchError: function (model, response) {
            model.deferred.reject();
            debug.log(response);
        },

        // Primarily a tool for unit tests... Don't rely on calling this.isReady!!
        isReady: function () {
            if (this.request) {
                return this.request.isResolved();
            } else {
                return this.deferred.isResolved();
            }
        },

        // **Method:** `setOptions` - set urlRoot
        setOptions: function () {
            if (this.options && this.options.urlRoot) {
                this.urlRoot = this.options.urlRoot;
            }
        },

        truncateString: lib.truncateString
    });

    return BaseModel;
});

Other BASE objects borrowing the same implementation

(base.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
// 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.

// Example use for a composite view utilizing addChildView, setOptions & callbacks:
// 
//     MyCompositeViewConstructor = BaseView.extend({
//     
//         template: myHTMLTemplate,
//     
//         initialize: function (options) {
//             _.bindAll(this);
//             this.setOptions();
//             BaseView.prototype.initialize.call(this, options);
//         },
//     
//         dataDecorator: function (data) {
//             data.myExtraProperty = 'stuff I added just in time to render';
//             return data;
//         },
//     
//         setOptions: function () {
//             var msg;
//             if (!this.options || !this.options.childView) {
//                 msg = "MyCompositeViewConstructor requires a options.childView object";
//                 throw new Error(msg);
//             }
//             this.addChildView();
//         },
//     
//         addChildView: function () {
//             var childView = this.options.childView, renderChildView;
//     
//             renderChildView = BaseView.prototype.addChildView(childView);
//             this.callbacks.add(renderChildView);
//         }
//     });

// Requires `define`
// Return `{BaseView}` constructor

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

    var BaseView,
        Backbone = facade.Backbone,
        $ = facade.$,
        _ = facade._,
        _toHTML = facade.toHTML,
        Deferred = facade.Deferred,
        Callbacks = facade.Callbacks,
        lib = utils.baselib,
        debug = utils.debug;

    // Constructor `{BaseView}` extends Backbone.Model.prototype
    // object literal argument to extend is the prototype for the BaseView constructor
    BaseView = Backbone.View.extend({

        // **Method:** `initialize`  
        // Param {Object} `options`  
        initialize: function (options) {
            if (options) {
                this.setOptions(options);
            }
            this.deferred = new Deferred();
            this.callbacks = Callbacks('unique');
        },

        // **Method:** `setOptions`  
        // Param {Object} `options`  
        // handle options passed to initialize, e.g. required properties/errors
        setOptions: function (options) {
            if (options.destination) {
                this.destination = options.destination;
            }
            if (options.template) {
                this.template = options.template;
            }
        },

        // **Method:** `render`  
        // Standarization of the task to render a view using a model & template
        // Options available:  
        // - Method to add the resulting markiup to the dom  
        // - Callback to mutate the model's data after model.toJSON() called  
        //   - Merging data to template happens after dataDecorator is applied  
        // - Callbacks object can be filled with ancillerary work following render
        //   - E.g. callbacks list can trigger rendering of child views  
        // Param {String} `domInsertion` - gives option for adding result to dom  
        // Param {Function} `dataDecorator` - accepts arg for {Object} `data`  
        // Returns the same (mutated) {Object} `data`
        render: function (domInsertion, dataDecorator, partials) {
            var markup;

            this.confirmElement();
            dataDecorator = dataDecorator || this.dataDecorator;
            markup = this.toHTML(dataDecorator, partials);
            domInsertion = this.domInsertionMethod(domInsertion);
            this.$el[domInsertion](markup);
            this.resolve();
            this.callbacks.fire(this.$el);

            return this;
        },

        // **Method:** `resolve`  
        // Resolve the view's deferred object after all callbacks are fired once.
        resolve: function () {
            var view = this;

            if (!this.deferred.isResolved()) {
                this.callbacks.add(view.deferred.resolve);
            } else {
                if (this.callbacks.has(view.deferred.resolve)) {
                    this.callbacks.remove(view.deferred.resolve);
                }
            }
        },

        // **Method:** `confirmElement`  
        // A view needs an `el` property and `$el` too; a helper to check that this.el is OK.
        confirmElement: function () {
            if (_.isUndefined(this.el)) {
                this.$el = $(this.options.el);
            }
            if (_.isUndefined(this.$el)) {
                throw new Error("View has no this.el or this.options.el property defined.");
            }
        },

        // **Method:** `toHTML`  
        // A wrapper for the task of merging a Mustache.js template with preprocessing
        // Handles the merging of JSON data from model with a HTML template {{vars}}
        // Prior to merging the template the data can be changed with dataDecorator  
        // Requires _toHTML an alias for the applications templating method  
        // Param {Function} `dataDecorator` - accepts and returns a {Object} `data`  
        // Param {Object} `partials` - see Mustache.js documentation.
        toHTML: function (dataDecorator, partials) {
            var markup, data, args;

            data = (this.model) ? this.model.toJSON() : null;
            if (dataDecorator && _.isFunction(dataDecorator)) {
                data = dataDecorator(data);
            }
            this.template = this.template || this.options.template;
            if (!this.template || !data) {
                throw new Error("BaseView method toHTML called, but this.template or data is not defined.");
            } else {
                markup = _toHTML(this.template, data, partials);
            }
            return markup;
        },

        // **Method:** `domInsertionMethod` - used when rendering to add markup to dom  
        // Default is `html` however this is configurable to support :  
        //  - 'append', 'html', 'prepend', 'text'
        domInsertionMethod: function (domInsertionMethod) {
            var defaultMethod = 'html',
                domInsertionMethods = ['append', 'html', 'prepend', 'text'],
                domInsertion;

            if (domInsertionMethod !== defaultMethod) {
                if (domInsertionMethod && _.isString(domInsertionMethod)) {
                    if (_.contains(domInsertionMethods, domInsertionMethod)) {
                        domInsertion = domInsertionMethod;
                    }
                }
            }

            return domInsertion || defaultMethod;
        },

        // **Method:** `dataDecorator`  
        // No-op re-define as needed as hook to modify model data just before rendering
        dataDecorator: function (data) { return data; },

        // **Property:** {Object} `callbacks` - list of callbacks
        // Child views or render stages can be managed using jQuery Callbacks
        // this should be an Callbacks object for each instance, so the 
        // initialize method sets the Callbacks object
        callbacks: null,

        // **Property:** {Object} `deferred` - implements a jQuery.Deferred interface
        // Initialization or other criteria to resolve whether view is ready 
        // can be handled with jQuery Deferred, this should be an deferred 
        // object for each instance, the initialize method sets the 
        // deferred instance.
        deferred: null,

        // Primarily a tool for unit tests... Don't rely on calling this.isReady!!
        isReady: function () {
            return this.deferred.isResolved();
        },

        // **Method:** `addChildView`  
        // For a composite view this method can add multiple view objects
        // Setup child views which can be rendered or appended to another context
        addChildView: function (view, context) {
            var callbackFn, msg;
            if (!view) {
                msg = "baseView addChildView expects view object as first arg.";
                throw new Error(msg);
            }
            if (context && !_.isEmpty(context)) {
                callbackFn = function () {
                    view.render();
                    view.$el.appendTo(context.$el);
                };
            } else {
                callbackFn = function () {
                    view.render();
                };
            }
            return callbackFn;
        },

        // **Method:** `getOuterHtml` - utility fn  
        // Using outerHTML with any browser via jQuery fallback when not supported
        getOuterHtml: function (obj) {
            return (obj[0].outerHTML) ? obj[0].outerHTML : $('<div/>').html( obj.eq(0).clone() ).html();
        },

        // **Method:** `destroy` - used to teardown a view object  
        // Best practice to avoid memory leaks is to remove references between objects
        destroy: function () {
            var key;

            if (this.removeSubscribers) {
                this.removeSubscribers();
            }
            this.$el.remove();
            if (this.destination) {
                $(this.destination).empty();
            }
            for (key in this) {
                delete this[key];
            }
        },

        // **Method:** `addSubscribers`  
        // No-op re-define as needed, for Channel pub/sub or other event bindings
        addSubscribers: function () {},

        // **Method:** `removeSubscribers`  
        // Re-define as needed used by this.destroy() to remove pub/sub bindings
        removeSubscribers: function () {
            this.$el.off();
        }

    });

    return BaseView;
});
(base.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
// Base Collection
// ---------------

// Requires `define`  
// Return {BaseCollection} object as constructor

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

    var BaseCollection,
        Backbone = facade.Backbone,
        $ = facade.$,
        _ = facade._,
        lib = utils.baselib,
        ajaxOptions = utils.ajaxOptions,
        debug = utils.debug;

    // Constructor `{BaseCollection}` extends Backbone.Collection.prototype
    // object literal argument to extend is the prototype for the BaseCollection constructor
    BaseCollection = Backbone.Collection.extend({

        // **Method:** `initialize`  
        // Param {Object} `models` - added during call to new BaseCollection([/*models*/])  
        // Param {Object} `options` - add a comparator
        initialize: function (models, options) {
            debug.log("BaseCollection initialize...");
            this.cid = this.cid || _.uniqueId('c');
            this.deferred = new $.Deferred();
            // When overriding use: `Backbone.Collection.prototype.initialize.call(this, arguments);`
        },

        // **Property:** `request` - assign fetch return value to this.request property, 
        // fetch returns (jQuery) ajax promise object
        request: null,

        // **Method:** `fetch`  
        // Wrap Backbone.Collection.prototype.fetch with support for deferreds
        fetch: function (options) {
            options = options || {};
            if (!options.success) {
                options.success = this.fetchSuccess;
            }
            if (!options.error) {
                options.error = this.fetchError;
            }
            _.extend(options, ajaxOptions);
            this.request = Backbone.Collection.prototype.fetch.call(this, options);
            if (!this.request) {
                this.request = this.deferred.promise();
            }
            return this.request;
        },

        // Primarily a tool for unit tests... Don't rely on calling this.isReady!!
        isReady: function () {
            if (this.request) {
                return this.request.isResolved();
            } else {
                return this.deferred.isResolved();
            }
        },

        // Default success and error handlers used with this.fetch() ...

        // **Method:** `fetchSuccess` - resolve the deferred here in success
        fetchSuccess: function (collection, response) {
            this.deferred.resolve(response);
            debug.log(response);
        },

        // **Method:** `fetchError` - log response on error
        fetchError: function (collection, response) {
            debug.log(response);
        }

    });

    return BaseCollection;
});

Using a base constructor

(events.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
// Events Collection  
// -----------------

// @requires `define`  
// @return {BaseCollection} EventsCollection instance

define(['jquery','underscore','models','collections/base','utils/debug'],
function ($,      _,           models,  BaseCollection,    debug) {

    var EventsCollection,
        eventsModel = new models.EventsData(),
        Event = models.Event;

    EventsCollection = BaseCollection.extend({
        model: Event,

        initialize: function (models, options) {
            var collection = this,
                data = eventsModel;

            collection.deferred.done(function () {
                EventsCollection.__super__.initialize.call(this, arguments);
                debug.log("EventsCollection initialized using eventsData.get('events').");
            });
            data.deferred.done(function () {
                collection.models = data.get("events");
                collection.deferred.resolve();
            });
        },

        comparator: function(event) {
            return event.get("id");
        },

        isReady: function () {
            return (eventsModel.isReady() && this.deferred.isResolved());
        },

    });

    return new EventsCollection();

});

Extending a Collection instance by inheriting it’s constructor

(events-segments.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
// Events Segments Collection  
// -----------------

// @requires `define`  
// @return {Array} `segments` multi-dimensional array[schedule][type] 
// with collection {EventsSegmentCollection} instances

define(['jquery','underscore','collections/events','utils/baselib','utils/debug'],
function ($,      _,           eventsCollection,    baselib,        debug) {

    var EventsSegmentCollection, types, schedules, segments;

    // Subclass for creating specific segments of a collection
    EventsSegmentCollection = eventsCollection.constructor.extend({

        model: eventsCollection.model,

        // @param `options` should have type and schedule properties to filter the collection by  
        // @param `models` is not really needed, (this is the args format Backbone uses)
        // when null/undefined is passed the model will filter the events collection using options param
        initialize: function (models, options) {
            if (!options && !options.type && !options.schedule) {
                throw new Error('EventsSegmentCollection expected options parameter with type and schedule properties.');
            }
            // type and schedule properties are used to filter the collection
            this.type = options.type;
            this.schedule = options.schedule;
            // use events collection models with falsy models argument, e.g. null
            this.models = models || this.selectFilters(options);
            // set this inherited property as resolved
            this.deferred.resolve();
        },

        // Methods to filter collection and match schedule and/or type
        // filter method is Backbone pointer to Underscore collection `filter` method
        // @see <http://documentcloud.github.com/backbone/#Collection-Underscore-Methods>
        // collection's properties `type` and `schedule` are setup during initialization 
        selectFilters: function (options) {
            var collection = this, models;
            if (options.type === 'All') {
                models = eventsCollection.filter(function (model) {
                    return (model.schedule && model.schedule === collection.schedule);
                });
            } else {
                models = eventsCollection.filter(function (model) {
                    var found = false;
                    if (model.schedule && model.schedule === collection.schedule) {
                        if (model.types && $.inArray(collection.type, model.types) !== -1) {
                            found = true;
                        }
                    }
                    return found;
                });
            }
            return models;
        }

    });

    // Create segmented collections from the events collection
    // e.g. 'events.today.Women', 'events.ending_soon.Home'
    // events objects are expected to have type and schedule properties
    // the strings in the schedules and type array match values on the event properties
    schedules = ['today', 'ending_soon' /*, 'upcoming' */ ];
    types = ['Beauty', 'Getaways', 'Home', 'Kids', 'Men', 'Women', 'All'];

    // segments are combinations of schedules and types, e.g. `today` and `Beauty`
    segments = {};

    // iterate over schedules and types to build new collections by segments
    _.each(schedules, function (schedule) {
        segments[schedule] = {};
        _.each(types, function (type) {
            segments[schedule][type] = new EventsSegmentCollection(null, {
                "type" : type,
                "schedule" : schedule
            });
        });
    });

    return segments;

});