Vertebrae framework

built with Backbone.js and RequireJS using AMD

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.

CollectionView

This constructor is based on the article by Liquid Media

Collection View (collection.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
// Collection View
// ---------------
// Manages rendering many views with a collection 
// See: <http://liquidmedia.ca/blog/2011/02/backbone-js-part-3/>

// The CollectionView extends BaseView and is intended for rendering a collection.
// A item view is required for rendering withing each iteration over the models.

// Requires `define`
// Returns {CollectionView} constructor 
// - instances must have a collection property

define(['facade','views/base','utils'], function (facade, BaseView, utils) {

    var CollectionView,
        $ = facade.$,
        _ = facade._,
        Backbone = facade.Backbone,
        debug = utils.debug;

    // Constructor `{CollectionView}` extends the BaseView.prototype
    // object literal argument to extend is the prototype for the CollectionView Constructor
    CollectionView = BaseView.extend({

        // **Method:** `initialize`  
        // Param {Object} `options` must have a child view and tagname  
        // - options should have properties: `view`, `tagName` 
        initialize : function (options) {
            var collection, msg;

            if (!this.collection || !(this.collection instanceof Backbone.Collection)) {
                msg = "CollectionView initialize: no collection provided.";
                throw new Error(msg);
            }
            BaseView.prototype.initialize.call(this, options);
            this._view = this.options.view || this._view;
            if (!this._view) {
                throw new Error("CollectionView initialize: no view provided.");
            }
            this._tagName = this.options.tagName || this._tagName;
            if (!this._tagName) {
                throw new Error("CollectionView initialize: no tag name provided.");
            }
            this._className = this.options.className;
            this._decorator = this.options.decorator;
            this._id = this.options.id;
            this._views = [];
            _(this).bindAll('add', 'remove');
            this.setupCollection();
        },

        // **Method:** `setupCollection`  
        // bindings for adding and removing of models within the collection
        setupCollection: function () {
            var collection = this.options.collection || this.collection;

            collection.bind('add', this.add);
            collection.bind('remove', this.remove);
            if (!collection.length && !collection.request) {
                collection.fetch();
                collection.request.done(function () {
                    collection.each(this.add);
                });
            } else {
                collection.each(this.add);
            }
        },

        // **Method:** `add`  
        // Param {Model} `model` object that extends Backbone.Model
        // Creates a new view for models added to the collection
        add : function(model) {
            var view;

            view = new this._view({
                "tagName": this._tagName,
                "model": model,
                "className": this._className,
                "decorator": this._decorator
            });
            this._views.push(view);
            if (this._rendered) {
                this.$el.append(view.render().el);
            }
        },

        // **Method:** `remove`  
        // Param {Model} `model` object that extends Backbone.Model
        // removes view when model is removed from collection
        remove : function(model) {
            var viewToRemove;

            viewToRemove = _(this._views).select(function(cv) {
                return cv.model === model;
            })[0];
            this._views = _(this._views).without(viewToRemove);
            if (this._rendered) {
                viewToRemove.destroy(); // $(viewToRemove.el).off().remove();
            }
        },

        // **Method:** `render`  
        // Iterates over collection appending views to this.$el
        // When a {Function} decorator option is available manipulte views' this.$el
        render : function() {
            this.confirmElement.call(this);
            this._rendered = true;
            this.$el.empty();

            _(this._views).each(function(view) {
                this.$el.append(view.render().el);
                if (view.options.decorator && _.isFunction(view.options.decorator)) {
                    view.options.decorator(view);
                }
            }, this);

            this.resolve.call(this);
            this.callbacks.fire.call(this);
            return this;
        }
    });

    return CollectionView;
});

Header view with factory method to generate multiple collection views

This view has a custom method buildCollectionView with is a factory for generating multiple collection view objects based on a multi-dimensional array containing segments of events by type and schedule. See the block that contains…

1
2
3
4
5
6
7
view[schedule][type] = new views.CollectionView({
    'collection': collections[schedule][type],
    'view': EventView,
    'tagName': 'li',
    'el': view.$('section.' + className + ' ul'),
    'className': className
});

The CollectionView constructor requires a Backbone collection instance and a Backbone Model constructor as well as properties tagname and el which are used when initializing instances of the view constructor for each model in the collection instance.

Full implementation example

Header View (header.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
/**
 * header view 
 * @requires define
 * @return {HL.View} constructor object
 */

define([
        'jquery','underscore',
        'collections',
        'views/base',
        'views',
        'packages/header/views/event',
        'packages/header/models/branding',
        'packages/header/views/branding',
        'packages/header/models/nav',
        'packages/header/views/nav',
        'utils/debug'],
function ($, _,
        collections,
        BaseView,
        views,
        EventView,
        brandingModel,
        BrandingView,
        navModel,
        NavView,
        debug) {

    // global header view
    var HeaderView;

    HeaderView = BaseView.extend({
        el: 'header',

        initialize: function (options) {
            var view = this,
                _el = view.el;

            options = options || {};
            view.schedules = options.schedules || ['today','endingSoon'];
            view.types = options.types || ['women','home','kids','beauty','men','all','getaways'];

            view.branding = new BrandingView({
                el : _el,
                model : brandingModel
            });
            view.nav = new NavView({
                el : _el,
                model: navModel
            });
            view.iterateSchedulesAndTypes.call(view, view.setupCollectionReference);

            debug.log('HeaderView init');
        },

        render: function () {
            this.branding.render();
            this.nav.render();
            this.iterateSchedulesAndTypes(this.buildCollectionView);
            this.iterateSchedulesAndTypes(this.renderCollectionView);
            debug.log('Header views rendered');
            return this;
        },

        // list of collections, set order of appearance in menu by order in arrays.  
        // the segmented `collections` object has multidimentional array 
        // using properties matching these strings. These properties can be set using
        // the options object as argument durig initialization.
        // TODO function to resort by members type prior to rendering
        schedules: null,
        types: null,

        iterateSchedulesAndTypes: function(callback) {
            var view = this;

            _.each(view.schedules, function (schedule) {
                _.each(view.types, function (type) {
                    callback.apply(view, [schedule, type]);
                });
            });
        },

        setupCollectionReference: function (schedule, type) {
            var view = this;

            if (!view[schedule]) {
                view[schedule] = {};
            }
            if (!view[schedule][type]) {
                view[schedule][type] = {};
            }
        },

        buildCollectionView: function (schedule, type) {
            var view = this, collection, className = view.setClassName(schedule, type);

            // `collections` object should have properties with event segments
            if (!collections[schedule] || !collections[schedule][type]) {
                throw new Error('buildCollectionView cannot access collection in: collecions.' + schedule + '.' + type);
            }
            view[schedule][type] = new views.CollectionView({
                'collection': collections[schedule][type],
                'view': EventView,
                'tagName': 'li',
                'el': view.$('section.' + className + ' ul'),
                'className': className
            });
            debug.log('created collection: ' + schedule + '-' + type);
        },

        setClassName: function (schedule, type) {
            var className = type.charAt(0).toUpperCase();
            className += type.slice(1);
            className += '-';
            className += schedule;
            return className;
        },

        renderCollectionView: function (schedule, type) {
            var view = this;
            view[schedule][type].render();
            debug.log('rendered collection: ' + schedule + '-' + type);
        }

    });

    return HeaderView;

});

Examples when Collection Views are used as a product’s variants for color and size

Information View (information.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
// information view
// ----------------
// manages many views on product page

define([
    'jquery',
    'underscore',
    'backbone',
    'mustache',
    'text!packages/product/templates/information.html',
    'views/collection',
    'packages/product/views/color',
    'packages/product/views/size',
    'packages/product/views/price',
    'packages/product/views/quantity'
], function($, _, Backbone, Mustache, information_template, CollectionView,
            ColorView, SizeView, PriceView, QuantityView) {

    return Backbone.View.extend({
        tagName: 'section',
        className: 'product',

        events: {
            'click img.swatch': 'updateColor',
            'click .size': 'updateSize',
            'click .cart': 'addToCart'
        },

        initialize: function(options) {
            _.bindAll(this, 'render', 'updateColor', 'updateSize');

            this.colors = new CollectionView({
                collection: options.colors,
                view: ColorView,
                tagName: 'li'
            });

            this.sizesCollection = options.sizes;
            this.sizes = new CollectionView({
                collection: this.sizesCollection,
                view: SizeView,
                tagName: 'li'
            });

            this.quantityView = new QuantityView({
            });

            this.priceView = new PriceView({
                collection: options.items,
                quantityView: this.quantityView
            });
        },

        render: function() {
            $(this.el).html(Mustache.to_html(information_template, this.model.toJSON()));

            this.colors.el = $('section.colors ul', this.el);
            this.colors.render();

            // sizes are rendered based off the color
            this.sizes.el = $('section.sizes ul', this.el);

            this.priceView.el = $('section.price', this.el);

            this.quantityView.el = $('section.quantity', this.el);

            return this;
        },

        defaultColor: function() {
            $('img.swatch', this.el).filter(':first').trigger('click');
        },

        updateColor: function(event) {
            this.color = $(event.currentTarget).attr('title');
            this.model.set({currentColor: this.color});

            this.sizesCollection.byColor(this.color);
            this.priceView.byColorAndSize(this.color, false);
        },

        updateSize: function(event) {
            var size = $(event.currentTarget).attr('value');
            this.model.set({currentSize: size});

            this.priceView.byColorAndSize(this.color, size);
        },

        addToCart: function(event) {
            this.model.addCurrentItemToCart();
        }
    });
});

The color collection and view used by the Information view which acts as a view manager

Colors Collection

Colors Collection (colors.js) download
1
2
3
4
5
6
7
8
9
define([
    'underscore',
    'backbone',
    'packages/product/models/color'
], function(_, Backbone, Color) {
    return Backbone.Collection.extend({
        model: Color
    });
});

Colors View

Colors View (color.js) download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define([
    'jquery',
    'underscore',
    'backbone',
    'mustache',
    'text!packages/product/templates/color.html'
], function($, _, Backbone, Mustache, colorTemplate) {
    return Backbone.View.extend({
        tagName: 'li',

        render: function() {
          $(this.el).html(Mustache.to_html(colorTemplate, this.model.toJSON()));

            return this;
        }
    });
});