Rendering Backbone collections in a view

Best practices in Backbone.js seem to come up a lot. If you do much searching for how to do something using Backbone, you’ll quickly come to someone on StackOverflow asking “what are the best practices for” doing whatever you’re trying to figure out how to do. That’s because Backbone, in spite of being billed in some circles as an MVC framework, is really just a library. It’s a powerful library, but it holds almost no opinions about how you should structure your application, or what the relationship between the various prototypes it provides (That’s Model, Collection, View, and Router, plus a few supporting mixins) should be. You’re not writing a Backbone app, you’re writing JavaScript with a few helpful abstractions.

So in the absence of opinionated software, developers come up with lots of rules for themselves, and then other developers try to find out what they are by asking about best practices. It’s the circle of life. And Backbone, after a few years on the scene, has accumulated a fair amount of wisdom in the form of best practices. The problem is that we still seem to be in that awkward time when there are lots of things we’re supposed to be doing, but not much in the way of automating those things.

Take, for example, rendering a view that has a collection attached to it. Here’s an article that sums up the process pretty well (look under “Views should keep track of the sub views”). The issue that the article addresses is that when a view’s data source is a collection, each model within the collection should have its own view, and that the collection’s view should know about the models’ views. The author keeps track of the sub-views in a pseudo-private property, and adds and removes them with view callbacks when the collection’s add and remove events fire.

As a side note, be careful about using the example code as-is. It contains some bugs. Most notably, his “removeOne” method:


removeOne: function(ticket) {
	this._viewPointers[ticket.cid].remove();
}

See the problem? He calls the view’s remove method, but when the collection’s view is re-rendered, the deleted view will be right back there again, since it’s still in the array of cached views. Presumably the app he copied the code out of handled this differently, but for our purposes we can just add another line that deletes the reference:


removeOne: function(ticket) {
	this._viewPointers[ticket.cid].remove();
	delete this._viewPointer[model.cid];
}

Anyway, the author of the article, who is listed as “Prateek,” starts off by saying “The most common backbone pattern is to have a model and a collection of models and initialize a view that initializes this collection.” My response is, if it’s so common, why do I have to write so much code to make it happen?

Abstracter!

The goal should be to make this as painless as possible, adding as little boilerplate as possible, while still making the solution reusable. It’s a plugin, duh.

So I extend Backbone.View with the addOne and removeOne methods (Notice that the view that inherits this needs to implement a collectionView property):


Backbone.CollectionView = Backbone.View.extend({
	addOne: function(model) {
		view = new this.collectionView({ model: model });
		this._viewPointers[model.cid] = view;
	},
	removeOne: function(model) {
		this._viewPointers[model.cid].remove();
		delete this._viewPointers[model.cid];
	},
	_viewPointers: {}
});

Prateek doesn’t really address how one should then handle rendering these views with the rest of the parent view. My solution is to keep it agnostic, and make a method that returns the views in the collection wrapped in a jQuery object:


Backbone.CollectionView = Backbone.View.extend({
	renderCollection: function() {
		var els = _.map(this._viewPointers, function(view){
			return view.el;
		});
		return $(els);
	},
	
	...
});

And finally, we’ll add a method that can be called to bind all the proper events, probably during the view’s initialize method:


Backbone.CollectionView = Backbone.View.extend({
	rendersCollectively: function() {
		var self = this;
		this._viewPointers = {}; // make sure we're starting over
		this.collection.each(function(model){
			self.addOne(model);
		});
		this.collection.on('add', function(model) {
			this.addOne(model);
			this.render();
		}, this);
		this.collection.on('remove', this.removeOne, this);
	},
	
	...
});

Rather than binding the view’s render method to the collection:change, we’re going to call it in the addOne method. This prevents the problem of the rendering taking place before the view has been added _viewPointers.

Using this plugin in an application would look something like this:


App.Views.EventIndex = Backbone.View.extend({
	initialize: function() {
		this.rendersCollectively();
		this.render();
	},
	render: function() {
		this.$el.html(this.template());
		this.$el.find('.eventList').html(this.renderCollection());
	},
	className: 'index',
	template: Templates.eventIndex,
	collectionView: App.Views.EventListItem
});

The complete plugin


(function($, Backbone){
	var oldView = Backbone.View;

	Backbone.CollectionView = Backbone.View.extend({
		renderCollection: function() {
			var els = _.map(this._viewPointers, function(view){
				return view.el;
			});
			return $(els);
		},
		rendersCollectively: function() {
			var self = this;
			this._viewPointers = {}; // make sure we're starting over
			this.collection.each(function(model){
				self.addOne(model);
			});
			this.collection.on('add', function(model) {
				this.addOne(model);
				this.render();
			}, this);
			this.collection.on('remove', this.removeOne, this);
		},
		addOne: function(model) {
			view = new this.collectionView({ model: model });
			this._viewPointers[model.cid] = view;
		},
		removeOne: function(model) {
			this._viewPointers[model.cid].remove();
			delete this._viewPointers[model.cid];
		},
		_viewPointers: {}
	});

	Backbone.View = Backbone.CollectionView;

	Backbone.CollectionView.noConflict = function() {
		Backbone.View = oldView;
	}

})(jQuery, Backbone);

This is still an early version of this mini-plugin (I wrote it last night), and I may have missed a few best practices here myself. Feel free to send me an email if you have any corrections.