More often than not your client-side application will be dealing with sets of data rather than single items of data, for example: users, pages, products, documents, etc. We have looked at how Backbone represents individual items of data as Models. In this chapter we will look at how to work with multiple items of data through Backbone Collections.
From the Backbone Documentation: "Collections are ordered sets of models." Using Collections you can easily sort and manipulate your data without fetching it afresh each time from the server. Take for example an application for managing the users of a blogging system. You may want to be able to see which users have authored the most articles, which users have written the most comments, or who the newest users are. In a traditional server-based application, each time you wanted a different view of the data you would have to load a new page from the server. This is not very fast, and not really necessary as the data hasn't changed, just the order in which it is displayed. If the application were built with Backbone, then there would be a collection of user models. Sorting the users would be a simple case of calling:
users.sortBy(function(user){ return user.get('number_of_posts'); });
This would sort the models by the "number_of_posts" field. You could easily resort the data by another field:
users.sortBy(function(user){ return user.get('date_joined'); });
Once the data has been sorted, it can be re-rendered on the page.
Backbone Collections inherit many methods from Underscore (see Chapter 1: The Setup for more information), which make it easy to sort, group, and filter your Models.
Collection Classes are defined in a simliar way to Models. They can be extended either from the base Backbone.Collection or from another Collection that you've defined. Here is a simple Collection Class for Issues:
// collections/issue.js
(function (window) {
'use strict';
var app = window.app;
var Backbone = window.Backbone;
var Issue = app.Issue = app.Issue || {};
Issue.Collection = Backbone.Collection.extend({
model: Issue.Model,
comparator: function (issue) { return -1 *
Date.parse(issue.get('created_at')); },
url: '/issues'
});
})(this);
Let's look at the three properties that we've defined.
For example, you could instantiate a new collection from the above class like this:
collection = new Issue.Collection();
To get data from the server you could call collection.fetch();
. This would issue an AJAX GET request to /issues
. The expected response would be an array of data items. When these items are returned from the server they would be converted into Issue.Model
s and added to your collection (ordered by date created). If you made a change to a model in the collection and that model had an ID of 10, then the PUT request to the server would be sent to /issues/10
.
Backbone is quite flexible when it comes to working with legacy APIs or services that are outside your control. You can define a parse
method to extract the actual array of data from your web service's response. For example if you received responses like this:
{"message": "Success", "date": "2013-01-10", "items": [
{"id": 1, "title": "First Issue"},
{"id": 2, "title": "Second Issue"}
]}
You could write a parse method as follows:
Issue.Collection = Backbone.Collection.extend({
model: Issue.Model,
url: '/issues',
parse: function(response){ return response.items; }
});
This method ensures that the collection will receive the array of data rather than response metadata.
You can define any custom methods on your collections in the same manner as you can with models. After defining and creating your collections, you need to be able to add and remove models from it, whether in response to an internal event, user input, or an update from the server.
Let's take a look at how you can add or remove models from collections. Backbone provides a variety of flexible methods to achieve this. It's important to remember that a model may be in several different collections at the same time. Collections usually contain a filtered subset of the data in your database, so adding and removing models from a collection is not equivalent to adding and removing data from your database. This is an important distinction between models and collections. Models usually have a direct mapping to the data in your database. You would normally persist changes in your model data to the server.
You can add existing Backbone Models or raw objects to Collections. Raw objects will first be converted to models using the model constructor defined in the collection class (or the standard Backbone.Model if no model constructor is defined). There are three basic methods for adding models:
These three methods operate completely independent of your server; however, often you want to add a model to a collection and at the same time save that model to your server. Perhaps you have a commenting system and someone is adding a comment. You want the new comment to immmediatly be in your comments collection and then asynchronously saved to the server. To do this you would use collection.create
. If you are syncing to a server with a REST API, then this method works as follows:
The above flow is an example of how Backbone helps you create ultra fast client-side applications. We can assume that most of your application's interactions with the server will be succesful. Therefore, there is no need to make the user wait while persisting data to the server. If there is an error then this can be handled by listening for the error events and notifying the user accordingly. If you would prefer to wait for the server's response before adding the model to the collection, then you can pass in {wait:true} as an option.
Often you may have existing data in your collection, but you want to update it with the latest data from the server. Backbone provides two methods for this:
Both reset
and update
can be called internally by the collections sync method. The default behaviour is to use reset
, so each time you call fetch
all models in your collection will be replaced. If {update:true}
is passed as an option to the fetch method, the update
method is used.
As an example to keep the data in your collection up to date, you could set up a setInterval
to fetch the data every one minute.
setInterval(function(){ issues.fetch({update:true}); }, 1 * 60 * 1000);
This code will run the fetch method every minute to get data from your server. We've passed update:true as an option, so every minute when the data is retrieved from the server it will be passed to the collection's update method. This method iterates through the data items and tries to match each item with existing models in the collection:
This allows you to efficiently update your views with only data that has been changed, rather than re-rendering your views every time data is fetched from the server.
In the Hubbub example appplication, we are working with the public GitHub API, which limits requests to sixty per hour. Because of this we haven't implemented any automatic fetching. Rather, fresh data is only fetched from the server when the user clicks the refresh icon for a repo.
Now let's take a look at the various remove methods that Backbone provides.
These methods remove models from the collection (but don't destroy the model, or the model data). Again, remember that collections aren't an exact mapping to tables in your database. You can have multiple collections of the same data ordered and filtered in different ways. There are three removal methods:
All of the above methods will trigger a "remove" event on the collection. In some ways you can view collections as arrays of models. All the usual array operators are available: push, pop, shift, unshift, length, as well as many utility functions for filtering and manipulating the collection's models. We've looked at how to get models in and out of collections, now lets take a look at sorting and filtering models once they are within a collection.
As we've already mentioned, Backbone Collections inherit a variery of useful filtering and sorting methods from Underscore. We're going to illustrate the power and simplicity of these methods through a series of tests. As explained in the testing chapter, tests are an important part of keeping your application bug-free and maintainable. Collections are quite easy to test because you are dealing with data rather than user interaction.
In our Hubbub application we need to be able to filter issues by their category and by which repo they belong to. Lets look at some examples of how to do this with some test data.
// test/generic.js
var testData = [
{"id": 10338616, "title": "Comparator and fat arrow", "number": 2195,
"repoId": 952189, "category": "doing", "comments": 5, "created_at":
"2013-01-26T14:35:16Z"},
{"id": 10341232, "title": "trigger calls unbinded event handlers",
"number": 2198,
"repoId": 952189, "category": "todo", "comments": 21, "created_at":
"2013-01-26T18:26:53Z"},
{"id": 10339785, "title": "All Code refactoring", "number": 2196,
"repoId": 952189, "category": "doing", "comments": 1, "created_at":
"2013-01-26T16:29:40Z"},
{"id": 10172489, "title": "Reverting changes from #2003 and 1f3f45252f",
"number": 2173, "repoId": 952189, "category": "done", "comments": 3,
"created_at":
"2013-01-21T21:36:58Z"}
];
var issues = new app.Issue.Collection(testData);
The above code creates a new Issues Collection with the four sample issues from the testData array. The following are all available in the test suite: http://backstopmedia.github.com/hubbub/test/index.html
To create the tests as you read along, create a file named "generic.js" in the test directory (these are generic Backbone tests rather than Hubub specific ones). Set up the test file with the following code:
window.jQuery(function () {
var module = window.module;
var _ = window._;
var app = window.app;
var ok = window.ok;
var test = window.test;
var equal = window.equal;
var deepEqual = window.deepEqual;
module("generic");
Then add the test data, and start adding the tests below.
// test/generic.js
test('sort by title', function () {
var issues = new app.Issue.Collection(testData);
equal(issues.at(0).get("title"), "trigger calls unbinded
event handlers");
// This is the first item before sorting
var sorted = issues.sortBy("title"); equal(sorted[0].get("title"),
"All Code refactoring");
// After sorting this item is first
});
This test creates a new Issue Collection with the test data. We first ensure that the models are correctly ordered by "created_at" when they are added. If no comparator had been set then the first models would be the first item in the array.
When a collection is instantiated any models added to it are kept in property named models
. While it is possible to access this array of models directly its usually preferable to use the at
method. So in the above test issues.at(0)
is equivalent to issues.models[0]
.
sortBy
is a method inherited from underscore.js. It accepts either an iterator function or a property key. For this simple sort, we can simply pass in "title", and a new array of models will be returned ordered by each model's title. Its important to note that this method and others like it don't affect the collection itself, they rather return a new array of filtered or sorted models.
Here is another test with an iterator function rather than a property key. This time the models are ordered by the length of their titles.
// test/generic.js
test('sort by longest title', function () {
var issues = new app.Issue.Collection(testData);
var sorted = issues.sortBy(function(issue) { return -1 *
issue.get("title").length; });
equal(sorted[0].get("title"), "Reverting changes from #2003
and 1f3f45252f");
// After sorting this item is first
});
As you can see it is fairly easy to sort the models in your collection. You could also create more complex sortBy interators to sort by two or three properties.
As well as sorting, Backbone Collections have several useful methods for filtering models. In our Hubbub application we need a collection that contains issue models that have the category: "done." For simple filters like this, we can use the where
method. This accepts a hash of keys and values to test models against, e.g. issues.where({category:"done"});
This would return all models with a property of category that is equal to "done."
For more complex queries we can use the following underscore methods.
The above three methods all take an iterator function, for example:
// test/generic.js
test('filter by title contains "event"', function () {
var issues = new app.Issue.Collection(testData);
var filtered = issues.filter(function(issue) {
return issue.get("title").indexOf("event") !== -1; });
equal(filtered.length, 1);
// Only one model found
equal(filtered[0].id, 10341232);
// Confirm correct model is returned
});
The function that is passed into the filter method is called with each model in the collection and should return the result of a test. If we had used reject
rather than filter
then the other three models would have been returned instead. You can use these functions to quickly and powerfully filter your collections. For example as the user is typing into a search box, you could filter a collection according to the text they are entering and update the results view instantly.
If you are doing a lot of complex filtering on your collections then there are some Backbone plugins that abstract a lot of the logic away for you and provide a query API similar to MongoDB: https://github.com/bevry/query-engine https://github.com/davidgtonge/backbone_query.
Sometimes you need to group the models in a collection according to a model property. There are two methods you can use for this.
countBy is similar to groupBy but deals with a common use case, simply adding up how many models there are in each group. These methods could be used to find out how many issues there are in each of the categories:
// test/generic.js
test('collection: groupBy & countBy', function () {
var issues = new app.Issue.Collection(testData);
var iterator = function(issue) { return issue.get("category"); };
var grouped = issues.groupBy(iterator);
var counts = issues.countBy(iterator);
deepEqual({ done:1, doing:2, todo:1 }, counts);
equal(counts.doing, grouped.doing.length);
});
Along with filtering, sorting, and grouping our collections, we need to be able to access the data in our collections, either to display to the user, or to use in our application logic. Let's take a look at some of the Underscore methods that can help with this.
Sometimes you need to retrieve specific values from each Model in a Collection, at other times you may want to check if a Collection meets certain criteria and perform an action accordingly. The next three methods don't return any specific data, rather they return true or false depending on if the collection passes a test.
These methods return a boolean:
Again to illustrate these methods, let's run some tests on our sample data. In the following code we're going to check if our collection contains models with various category values.
// test/generic.js
test('collection: some', function () {
var issues = new app.Issue.Collection(testData);
var hasToDoIssues = issues.some(function(issue) {
return issue.get("category") === "todo"; });
var hasRejectedIssues = issues.some(function(issue) {
return issue.get("category") === "rejected"; });
equal(hasToDoIssues, true);
// This variable should be true as there are issues with a
todo category
equal(hasRejectedIssues, false);
// This variable should be false as there are no issues with a
rejected category
});
You can see that "hasToDoIssues" is true because there is at least one issue with the category of "todo." "hasRejectedIssues" on the other hand is false, as our collection doesn't contain any models with the category of "rejected".
Now let's look at getting data from each model in our collection.
The next two methods iterate through all the models in a collection and return the requested properties as an array.
Pluck is a simpler version of map and often provides a succinct way of getting the data you require. Here is a test demonstrating the use of map
and pluck
to generate an array of issue numbers.
// test/generic.js
test('map & pluck', function () {
var issues = new app.Issue.Collection(testData);
var mapped = issues.map(function(issue) { return issue.get("number"); });
var plucked = issues.pluck("number");
equal(plucked.toString(), mapped.toString());
// Mapped and plucked arrays should be the same
equal(plucked.length, 4); // Should have 4 values
equal(plucked[0], issues.at(0).get("number"));
// First value should be equal to the number property of the
first model
});
You can see that we used both methods to achieve the same goal. "Map" is more powerful however, you could perform various calculations in the iterator function and return the result of those, rather than a staight property value.
The next set of methods return either a single value or a single model from a collection.
These methods are especially useful if your models contain properties with numerical values:
You could use reduce
to add up the total number of comments of all the issues in a collection:
// test/generic.js
test('collection: reduce', function () {
var issues = new app.Issue.Collection(testData);
var reduceIterator = function(sum, issue) { return sum +
issue.get("comments"); }
var numberOfComments = issues.reduce(reduceIterator, 0);
// We pass an iterator function and the starting "memo" value
equal(numberOfComments, 30);
// The total number of comments from all the issues.
});
This method works by iterating through all of the models in the collection and adding the "comments" value to the result of the last iteration. We pass in 0 to the method, so that the first iteration has something to add to. Each iteration should return the adjusted value ready for the next iteration.
max
and min
could be used to find the issue with the most or least comments:
// test/generic.js
test('collection: max & min', function () {
var issues = new app.Issue.Collection(testData);
var iterator = function(issue) { return issue.get("comments"); };
var mostCommentsIssue = issues.max(iterator);
// We can use the same iterator for both the min and max methods
var leastCommentsIssue = issues.min(iterator);
equal(mostCommentsIssue.id, 10341232);
equal(leastCommentsIssue.id, 10339785);
});
Sometimes you don't need to extract any data from your collection, rather you want to operate on all of the models within that collection. Backbone provides two methods for this:
The invoke
method can help keep your code terse where you need to call the same function on all models. For example this code: issues.invoke('fetch');
will call fetch on all the models in the collection. This is clearer to read and less verbose than: issues.each(function(model) { model.fetch(); });.
Learning to use the underscore methods with your collections can hopefully keep your code clean and stable. Another important principle in keeping your application lean and maintainable is the correct usage of events. As we've discovered earlier, Backbone provides a powerful events system, lets take a look at some of the Collection specific events that are built into Backbone.
Events are an important part of Backbone Collections. Each collection has access to the same event methods as Models
, Views
and Routers.
For an explanation of the various event methods (on, off, etc.) please refer to Chapter 3: Backbone Events.
Events are a flexible way of strucuturing your app and help a lot with keeping your code modular. For example 1 or 20 views could listen to events on a collection, without you needing to change any of the code for that collection.
You can trigger any custom events on your collections, but the following events are built into Backbone:
Any event that is triggered on a model in the collection is also triggered on the collection itself. For example if the "category" attribute of a model is changed then "change:category" would be fired on that model and on the collection that it belongs to.
In the Hubbub application, we use many of these events to ensure that collections stay in sync with views and with other collections. To illustrate how events work lets look at the setFilter
method in the issue collection class.
We use this method to create filtered collections for the different issue categories. The idea is that there is a "master" collection that contains all of the issues. There are then separate "filtered" collections that contain only a subset of those issues based on category.
If the issue's categories weren't going to change, we could simply use the where
method described above to filter the issues and add them to the appropriate collection. However we want the filtered collections to automatically stay updated when a model's category changes and when models are added or removed from the master collection:
// collections/issue.js
setFilter: function(parent, testKey, testValue) {
var self = this;
var onAdd = function(model) {
// only add to this collection if the model passes the filter
if (model.get(testKey) === testValue) {
self.add(model);
}
};
// If there are any existing models in the collection run them
// through the add parent.each(onAdd);
this.listenTo(parent, {
// When a model is added to the parent, add it to this
// collection if it matches the test key / value
add: onAdd,
// When a model is removed from the parent, remove it from this
// collection.
// This method is a "no op" if the collection doesn't contain the model.
remove: self.remove
});
// Listen to change events on the models
this.listenTo(parent, 'change:' + testKey, function(model, value) {
if (value === testValue) {
// If the new value matches the test value then add the model
self.add(model);
} else {
// If the value doesn't match the test value then remove the model
self.remove(model);
}
});
}
The method accepts a "master" (parent) collection and a key and value to test models against. Let's test the code to ensure that it works as planned:
// test/hubbub.js
test('collection: setFilter method', function () {
var issues = new app.Issue.Collection(testData);
var done = new app.Issue.Collection();
done.setFilter(issues, "category", "done");
equal(done.length, 1);
// Contains just the one model with a done category
issues.get(10172489).set("category","doing");
equal(done.length, 0); // Contains no models as the one model with a 'done' key had its category changed to doing
issues.invoke("set", "category", "done");
equal(done.length, 4); // All 4 models are now, in the done collection });
You can see that issue models are automatically added to and removed from the "done" collection as the models' category attribute is changed.
Once collections and views are set up to listen to the correct events, then simply changing an attribute on a model will cause everything to be updated correctly.
Like Models, Collections allow you to define class-level properties/methods. Let's start by defining a Collection for our list of repositories:
// collections/repo.js
(function (window) {
'use strict';
var app = window.app;
var Backbone = window.Backbone;
var Repo = app.Repo = app.Repo || {};
Repo.Collection = Backbone.Collection.extend({
model: Repo.Model,
// sort by owner login name, then repo name
comparator: 'full_name'
});
})(this);
In Hubbub, we want to keep track of the list of repos the user has added to their board. Therefore, let's define a Repo Collection as a property on a Board:
// models/board.js
initialize: function () {
this.repos = new app.Repo.Collection();
// ...
}
In this case, we are instantiating a Collection the normal way: with the new keyword. In Hubbub, we want our users to be able to search not only by a full Github repo path ("login/repoName"), but also to choose from all repos by a particular Github user. Let's use a factory method!
A Factory method is a design pattern where objects ("products") are created through a proxy method. That function can also handle any special setup on or related to the new instance.
// test/hubbub.js
test('app.Repo.Collection.withOwner() should set the `url`', function ()
var repos = app.Repo.Collection.withOwner('bob');
equal(_.result(repos, 'url'), 'https://api.github.com/users/bob/repos');
});
Now, let's define withOwner() as a method on the class, which creates and returns a specialized app.Repo.Collection for us:
// collections/repo.js
Repo.Collection = Backbone.Collection.extend(
// instance methods
{
// ...
},
// class methods
{
withOwner: function (login) {
var repos = new Repo.Collection();
repos.url = app.apiRoot + '/users/' + login + '/repos';
return repos;
}
}
);
In this case, the factory method always returns an app.Repo.Collection, but you can imagine how it could create different kinds of classes, based on parameters provided, etc.
Backbone Collections allow you to easily group, sort and filter your models. It's important to see them as far more than an array of rows from your database. Rather collections are dynamic and live, they respond to and trigger their own events. They also often contain the URLs for syncing the client and server data. In the next chapter we will cover the use of persistence in Backbone with Sync, so that every page reload doesn't start your application over from scratch.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.