JavaScript Architecture: Backbone.js Collections

01.04.2012

Updated Aug 11, 2012 to reflect current library versions.

Collections of items are amazingly pervasive in applications. Gmail deals with collections of emails. Twitter deals with collections of tweets. Facebook deals with collections of friends, updates, and apps. Very often these collections contain living data. The app may be constantly updating with new, changed, or removed items from the server. Maybe users are able to filter, sort, add, edit, or delete items and maybe they can do so from multiple views that need to be synchronized.

Usually when we think of collections of items in software terms with think of an array. Objects can be added and removed from an array easily enough but a native array on its own has no ability to broadcast notice of the change. Why do we care? Let’s say we’re building an RSS reader that shows the user a feed of articles from their subscriptions. The articles come from an array we loaded from the server. Each article contains a remove button next to it that allows the user to remove the article from the feed. Let’s also say in the top-right corner of the screen, separate from the feed of articles, we have a little counter that shows the user how many articles they currently have remaining in the feed. Assume that the feed has a JavaScript object managing it and the counter has a separate JavaScript object managing it.

When the user clicks a remove button to remove an article from the feed, it’s easy enough for us to remove the article from the feed itself and the accompanying array of articles, but how do we update the counter? The most direct approach is to give the feed view a reference to the counter view. That way the feed can just call a function on the counter view to tell it to refresh itself, right? Noooooooooo! Sure, it might function, but that’s a very good way to couple our views and reduce our flexibility. Our views would know more than necessary about each other. In this case, the data should drive the views instead of the views driving each other. Unfortunately, it’s difficult for our data (the array of articles) to drive the view using a native array because the counter can’t “watch” the array for a removal of an article. Native arrays do not broadcast events when items are added or removed.

You’ll notice a great similarity between what I’ve written above regarding native arrays resulting in view coupling and what I wrote in my previous post about native objects resulting in view coupling. Indeed, there are a lot of similarities and they are both resolved by implementing the observer pattern within our data structures. Just as a native object can be wrapped by a Backbone model in order to broadcast attribute changes, an array can be wrapped by a Backbone collection to broadcast additions and removals.

Eventful collections

Collections trigger events when models are added or removed. Let’s take a look at how this might look:

// Create individual models.
var taleOfTwoCities = new Backbone.Model({
	title: 'A Tale of Two Cities',
	author: 'Charles Dickens',
	publisher: 'Chapman & Hall'
});
 
var goodEarth = new Backbone.Model({
	title: 'The Good Earth',
	author: 'Pearl S. Buck',
	publisher: 'John Day'
});
 
// Create collection, passing in A Tale of Two Cities that will make up
// our initial collection.  Alternatively, we could have passed in nothing 
// and then called books.add(taleOfTwoCities) later.
var books = new Backbone.Collection([taleOfTwoCities]);
 
// Set up the handler for when a model is added.
var onAdd = function(book, books, options) {
	alert('Book ' + book.get('title') + ' added at index ' + options.at + '. ' +
		'The collection now contains ' + books.length + ' models.');
};
 
// Set up the handler for when a model is removed.
var onRemove = function(book, books, options) {
	alert('Book ' + book.get('title') + ' removed from index ' + options.index);
}
 
// Set up event listeners.
books.on('add', onAdd);
books.on('remove', onRemove);
 
// Add good earth and index 0 (before the Tale of Two Cities).
books.add(goodEarth, {at: 0});
// Remove both books.
books.remove([taleOfTwoCities, goodEarth]);
// We could have removed just a single book like this:
// books.remove(goodEarth);

I’ve added several comments inline, but let’s review what’s going on here. When we add a model to the collection, we use the add() function. Optionally, you can pass the index where you want the model to be added. We specify 0 so goodEarth will be placed at the start of the collection. By default, it will be added at the end. When we add the model, the add event is automatically triggered by the collection and any respective handlers are called. In our case, the onAdd function is called. The three arguments are:

  1. The added model.
  2. The collection to which the model was added.
  3. Any options used to add the model.

You can add one or more models at a time.

We then removed both our book models at the same time. Even though we remove them using a single remove() call, the onRemove handler is called twice–once for each model being removed. The three arguments are:

  1. The removed model.
  2. The collection from which the model was removed.
  3. Any options used to remove the model.

You can remove one or more models at a time. Notice that even though we didn’t explicitly pass any options object as a second parameter to remove(), the options argument coming into onRemove does exist and is populated with the index at which the model resided.

The above code produces the following alerts, in order:

  1. Book The Good Earth added at index 0. The collection now contains 2 models.
  2. Book A Tale of Two Cities removed from index 1.
  3. Book The Good Earth removed from index 0.

What if you wanted to know when the title of any model in the collection changes? Looping through all the models and adding an event listener to each one is a pain, especially when we have to remember to always add event listeners to models being added and remove event listeners from models being removed. Fortunately, Backbone collections bucket-brigade events coming from the models themselves which can be very handy. Let’s see how this works:

var taleOfTwoCities = new Backbone.Model({
	title: 'A Tale of Two Cities',
	author: 'Charles Dickens',
	publisher: 'Chapman & Hall'
});
 
var goodEarth = new Backbone.Model({
	title: 'The Good Earth',
	author: 'Pearl S. Buck',
	publisher: 'John Day'
});
 
var books = new Backbone.Collection([taleOfTwoCities, goodEarth]);
 
var onTitleChange = function(book, value) {
	alert('Book title changed from ' + book.previousAttributes().title + ' to ' + value);
};
 
books.on('change:title', onTitleChange);
goodEarth.set({title: 'Good Earth, The'});

Notice we didn’t add an event listener for change:title on each individual book model, but only on the collection containing the book models. I even threw a little bonus nugget in there to show how we can retrieve the previous title.

Model configuration

So far we’ve been instantiating our own Backbone models before adding them to a collection. However, we can configure our collection to use a certain model class. We can then just pass a native object to the collection. Under the hood, the collection will then instantiate the configured model class for us and pass our native object into its constructor:

var Book = Backbone.Model.extend({
	defaults: {
		genre: 'historical'
	}
});
 
var BookCollection = Backbone.Collection.extend({
	model: Book
});
 
var books = new BookCollection();
 
books.add({
	title: 'A Tale of Two Cities',
	author: 'Charles Dickens',
	publisher: 'Chapman & Hall'
});
 
books.add({
	title: 'The Good Earth',
	author: 'Pearl S. Buck',
	publisher: 'John Day'
});

Notice in this case we actually extended Backbone.Collection and set the model property. Another option is to just set the model property on a newly instantiated collection:

var books = new Backbone.Collection();
books.model = Book;

If you don’t configure the model class, Backbone.Collection uses Backbone.Model by default. Configuring a collection with a model class may not seem very beneficial at the moment, but hopefully it will make more sense when talking about loading data into a collection.

Persistence

Similar to Backbone models, collections also have the ability to fetch data from persistence. Let’s see how this works:

var Book = Backbone.Model.extend({
	defaults: {
		genre: 'historical'
	}
});
 
var onSuccess = function(books) {
	var firstBook = books.at(0);
	alert(firstBook.get('title') + ' is of genre ' + firstBook.get('genre'));
};
 
var books = new Backbone.Collection();
books.model = Book;
books.url = '/books.php';
books.fetch({
	success: onSuccess
});

Like our previous example, we’ve configured a collection to use the Book model class. We’ve also configured an endpoint url of /books.php. When we call fetch(), the collection will issue a GET request to the endpoint. In my case, I’ve set up the endpoint to return a JSON array of books. Once the JSON is returned, the collection will loop through the array, instantiating and populating a model (in our case, a Book model) for each item. Then, the model will be added to the collection itself.

The example above will alert A Tale of Two Cities is of genre historical. The interesting part here is the genre was never sent from the server. Instead, the model picked up the default genre, historical, because we extended Backbone.Model to create our Book class and set the default genre there. Hopefully you can now see the benefit of configuring collections to use a specific model class.

Additional functionality

We’ve only covered a subset of the functionality of Backbone collections. Beyond what we’ve discussed, collections can be sorted and reset. Models can be retrieved by id. Models can be both saved to the server and added to the collection in one method call.

Collections can also be configured, for example, to load and parse data in a specific manner.

Beyond that, all collections have a bucketload of underscore functions baked in. In JavaScript Architecture: Underscore.js, I showed an example of how we can get an array of usernames from an array of user objects using the Underscore pluck() function:

var usernames = _.pluck(users, 'username');

Since collections have Underscore functions baked in, we can likewise get an array of all titles of the books in the collection in an even more concise manner:

var titles = books.pluck('title');

Nice! There are many other Underscore functions baked into collections you should check out as well. These will make your life easier and trim down your code.

Read more in the Backbone.Collection documentation.

<< JavaScript Architecture: Backbone.js ModelsJavaScript Architecture: Backbone.js Views >>

Tags: , , , , , , ,


Comments

01.06.2012 / Trouble said:

Wow!! Great Series! i read all of them! waiting to see soon some Articles about Views and Router!

Thanks a lot Mr. Hardy

06.27.2012 / jianmeng said:

Nice article. I follow Derick Bailey’s article(http://lostechies.com/derickbailey ) to here.

Anyone who want read more,visit Derick Bailey.

Thank you great works.

06.27.2012 / Aaron Hardy said:

Thank you jianmeng. Yes, Derick Bailey is really great. I likewise recommend everyone follow his blog and follow him on Twitter (http://twitter.com/#!/derickbailey). Also, I love Marionette (https://github.com/derickbailey/backbone.marionette) which cuts the boilerplate of backbone and provides nice structure and functionality.


Leave a Comment

Your email address is required but will not be published.




Comment