Backbone's documentation clearly and succinctly explains what Models are:
"Models are the heart of any JavaScript application, containing the interactive data as well as a large part of the logic surrounding it: conversions, validations, computed properties, and access control. You extend Backbone.Model with your domain-specific methods, and Model provides a basic set of functionality for managing changes."
Before we look in more detail at how Backbone Models work, it's important to understand why they are needed. Backbone follows the MV* architecture of separating your data (models) from how it is displayed and interacted with (views). While you could build a small JavaScript application without following any architecture, it is likely that it would be brittle and hard to maintain. There are many great resources on the MVC / MV* architecture available online, and you may well have used an MVC framework on the server, e.g. Ruby on Rails, Zend Framework, MVC ASP.Net, etc.
The key principle to take from this is that your data and its relevant logic should be separated from your views--how you display that data to the user.
Lets take a traditional HTML page that we could convert into an application. In a standard page, data is often displayed using a table:
<table>
<thead>
<tr><th>ID</th><th>Title</th><th>Category</th></tr>
</thead>
<tbody>
<tr><td>43</td><td>Fix welcome view</td><td>Doing</td></tr>
<tr><td>15</td><td>Add tests</td><td>ToDo</td></tr>
<tr><td>20</td><td>Update readne</td><td>Done</td></tr>
</tbody>
</table>
This is fine if all we want to do is display the static data. If you want to interactively edit, filter or sort this data, however, we need to use JavaScript. Using a DOM manipulation library such as jQuery, we could select and allow the user to interact with the data. We could implement a method to sort the table, by looping through all the rows, detaching them and inserting them in a sorted order. To find the right order we would need to extract the relevant data from the HTML, sort it in JavaScript and then rearrange the associated rows. If we wanted to allow the user to edit the data, we'd need to start adding click handlers and perhaps modal dialogs to enter the new data in. To save the data, we need to use AJAX requests. We are likely to end up with a JavaScript file full of functions, with logic about our data scattered throughout. We'd probably still be using the HTML table as the main representation of our data. We'd parse the data each time we need to update, change or sort the table. While this application may work, the code is likely to be a mess of unmaintainable spaghetti.
This is where Backbone comes to the rescue. Backbone provides a clear separation of concerns between the different parts of your application. Backbone Models contain your data and relevant logic (e.g. validation), while Backbone Views contain the display and user interaction code. Part of the core philosophy of Backbone is to not store data in the DOM. In fact the first step of creating a maintainable client-side application is to move the data out of the DOM. Instead we can use JavaScript objects (Backbone Models) to hold our data and we use Backbone Views to output this data to HTML. Using this approach we can ensure that there is a single authoritative place that the data exists in your app. There could be multiple views of this data, and when the data is updated, all of these views can be updated to show the changes. Where data is changed after an AJAX request from a server, or after a user input, there is one place that the change is made--the model.
Now that we've had a look at why we need Models, let's look at how they work in Backbone.
Backbone Models come with many useful methods for working with interactive data. Before we look at adding our own custom methods, let's go through and look at the built-in methods.
In current versions of JavaScript, there is no way of knowing when an object's property has been changed (there is a proposal for this functionality with "Object.observe" in ECMAScript 6 - "Harmony," however). Let's take the following example:
var issue = {id:12, title:"Fix welcome view", category:"doing"};
issue.category = "done";
There is no way for any view to know that the issue's category has changed. To get around this problem, Backbone Models have get()
and set()
methods to access their data. The same example with a Backbone Model would look like this:
var issue = new Backbone Model({id:12, title:"Fix welcome view",
category:"doing"});
issue.set("category","done"); //this changes the category attribute and triggers a change event
When the issue's category is changed, any view displaying that issue can be notified of the change via the change event, and update itself accordingly. Events aren't the only benefit we get from using the set() method; Backbone Models can also run validation methods each time data is changed. Before we look at set() in more detail, let's look at how we can retrieve data from Backbone Models.
Let's first look at the data retrieval methods.
As we illustrated earlier, get accepts a key and returns the associated value from the model's attributes. e.g. model.get('category').
get is the simplest and probably the most-often used of a Model's methods. While at first it may seem a bit clunky compared to accessing an object's attributes directly, it offers far greater flexibility. Later in this chapter we'll look at using the get method to retrieve computed properties as well as just the raw data.
This HTML escapes the attribute before returning it. This is necessary if you are allowing user-generated content to be inserted into your page as it will help prevent cross site scripting (XSS).
This utility method checks to see if the model contains a value for this key. This method can be useful in checking if a model has a property even if that property's value is false. For example:
test('Model#has', function() {
var model = new Backbone.Model({ read: false });
ok(model.has('read'), 'Property exists');
ok(!model.get('read'), '... but is not truthy'); });
This test will pass, showing that the "read" property exists on the model, even though it's value is false.
Backbone Models are often representations of rows or documents in a database on your server. Backbone assumes that if the data is from your sever then it will have some sort of unique id attribute. By default this attribute's name is id
, however it can be set to be a different key in your model class definition. If a model doesn't have an id value then it is assumed to be new and that it hasn't yet been saved to the server.
This method returns a copy of the models properties, ready to be turned into a JSON string. If you pass a model into JSON.stringify
then interally the toJSON
method will be called and a correct JSON string will be generated of the model's attributes. Sometimes this method is also used when sending data to the templates. It's important to note that this method uses "underscore clone" under the hood and therefore returns a "shallow copied" clone of the attributes. Any nested objects or arrays will be copied by reference, not duplicated.
Now lets take a look at editing the data in your models.
This method allows you to add or edit the data in your model. You can call it in one of two ways, and they have the same effect.
model.set("key","value", options);
model.set({key:"value"}, options);
You can also update multiple properties at the same time:
model.set({key:"value", key2:"value2"}, options);
The last argument: "options", is optional. If you don't want any change events to be fired you can pass in {silent:true} as an option.
If you pass in {validate:true} as an option, then any data you pass into "set" will first be validated. If the validation fails then the method will return false and no data will be changed on the model.
This method removes a key and its associated value from a model.
You can call save without any arguments to persist the data already in your model, for example:
model.save();
You can also pass in attributes in the same manner as with "set." For example:
model.save({key,value}, options);
The save method will use the Sync method that you've defined to save the model's data to the server. Please read Chapter 7: Sync for more details on how this works.
You can pass success and failure callbacks in the options object. These will in turn be passed to the sync method--in the default AJAX based sync they will be sent to jQuery.ajax.
This uses the sync method to get the latest data from the server. If the model is part of a collection, then the url endpoint is derived from the collection. If the model is not part of a collection then you can define a "urlRoot" property on the model to define the URL for sync operations.
This attempts to delete the data from the server, in the default "sync" implementation, and an HTTP "DELETE" request is sent to the server.
Along with Views, Collections and Routers, Models have an extend method to enable you to create your own model classes.
In the Hubbub application we use three different models: Board, Issue and Repo. Each of the issues in the application will be represented by an Issue model. Let's build this model first.
// models/issue.js
(function (window) {
'use strict';
// Model code to go here
})(this);
All of the modules in Hubbub are defined within an "Immediately Invoked Function Expression." This ensures that variables are kept from polluting the global namespace. Before we define the model we need to first grab some local references to the global "app" and "Backbone" objects. We are using the "app" object to store all of our model, collection and view methods. There will be models and collections for issues and these will both be stored in app.Issue.
var app = window.app;
var Backbone = window.Backbone;
var Issue = app.Issue = app.Issue || {};
Now that we've got references to our global objects we can define the model:
Issue.Model = Backbone.Model.extend({
defaults: {
category: 'default'
},
initialize: function () {
this.repo = this.collection.repo;
},
url: function () {
return this.urlRoot() + '/' + this.get('number');
},
urlRoot: function () {
return this.repo.url() + '/issues';
}
});
To understand this code we need to understand the structure of the appplication:
With this structure in mind we can have a closer look at the four instance properties for the issue model:
There are a few other Model properties that we could have set when defining the model:
This is an optional method which can be used to clean incoming data from the server. For example:
parse: function(response) {
return response.data;
}
This is useful when dealing with third party APIs, over which you have no control.
By default this is set as "id," if you use another key name for your "primary key" then you can set it here. For example MongoDB and CouchDB often use "_id". Backbone uses the presence of an id in a model to determine whether the model is "new" or not. A new model is assumed to contain data that has yet to be synced to the server.
We'll look at this in a bit more detail, but essentially this optional function validates any changes made to your data.
Once you have defined your Model classes you can create model instances by calling new Model
. The Model constructor accepts two arguments: attributes and options. Attributes is the data for your model, while options can contain:
It's possible to define a "validate" function on a model so that data attributes will only be saved to the model (or saved to the server) if the validate function passes. A validate function passes if it returns a false value, and fails if it returns anything else, such as a string. For example, we could say that the name attribute of a repository is required, like this:
// models/repo.js
Repo = app.Repo = app.Repo || {};
Repo.Model = Backbone.Model.extend({
validate: function(attributes, options) {
if (!attributes.name) { return 'Repo must have a name attribute'; }
} });
Then it would be impossible to save this model without giving it a name first. Add this test demonstrating how validate works, given the above model:
// test/hubbub.js
window.jQuery(function () {
module('Hubbub');
test('repo name is required', function () {
var repo = new app.Repo.Model();
// validate doesn't get checked in set() by default
// (it only gets checked in Model#save() by default)
repo.set('foo', 'bar');
equal(repo.get('foo'), 'bar');
// setting with the "validate" option shouldn't work here
// because validation will vail without a name attribute
repo.set('foo', 'baz', { validate: true });
notEqual(repo.get('foo'), 'baz');
// but if we set a name attribute, validation will pass
repo.set({
'name': 'test_name',
'foo': 'baz'
}, { validate: true });
equal(repo.get('foo'), 'baz');
});
});
// test/index.html
<head>
...
<script src="hubbub.js"></script>
</head>
It's common to have some piece of data that you frequently want to access from a model that is actually comprised of two or more other attributes on the model. The canonical example of wanting a computed field like this is wanting a User model to have a "full_name" attribute such that it always returns the model's "first_name" and "last_name" attributes appended together with a space.
We'll walk through a similar example from our Hubbub application where we want to easily access a repository owner's username with the repository name when persisting our "board" to local storage.
Backbone leaves you with a few valid options to do this. We'll show each of the options, using tests.
The easiest way to make this work is by just creating a new method on your model. Here's a snippet of the "toBoard" method from our completed Repo Model in Hubbub:
// models/repo.js
toBoard: function () {
var attrs = _.pick(this.attributes, 'id', 'name');
attrs.owner = {login: this.get('owner').login};
return attrs;
}
This custom method extracts the only data that we need in order to identify the repo: its id, name and owner's login name.
Let's add a test showing the proper behavior:
// test/hubbub.js
test('repos toBoard method products correct data', function () {
var repo = new app.Repo.Model({
id: 1,
name: 'hubbub',
full_name:'backstopmedia/hubbub',
owner: {login: 'backstopmedia'}
});
deepEqual(repo.toBoard(), {
id: 1,
name: 'hubbub',
owner: {login: 'backstopmedia'}
});
});
Sometimes you want to have computed properties available in a similar manner to actual properties. In this case, you can override the Model's get method. If Github didn't return the "full_name" of a repo and we wanted to create it as a computed property, then we could do this:
Repo.Model = Backbone.Model.extend({
get: function(attr) {
if (attr === 'full_name') {
return this.get('owner').login + '/' + this.get('name');
}
return Backbone.Model.prototype.get.apply(this, arguments);
}
});
Then we could simply call repo.get('full_name') like you would for any "real" attribute. Though if you wanted this attribute to also show up when you call Repo model's toJSON(), you would have to add it in there also.
We don't generally recommend using this approach though, because it can be less clear what is going on, and can get messy in larger projects.
Backbone.Compute and Backbone.Mutators are two plugins to help facilitate easier ways to use computed properties on models. Check out their READMEs for examples.
In simple applications like Hubbub it made sense to manually link up all the relations between models (and collections) in their initialize functions. An example of that can be seen in our Repo model, where we instantiate a Collection of Issues belonging to that Repo:
// models/repo.js
Repo.Model = Backbone.Model.extend({
initialize: function () {
this.issues = new app.Issue.Collection();
this.issues.repo = this;
}
);
The first line in initialize creates the connection from the Repo to the Issues collection. The second line creates a connection from the Issues collection back to its parent Repo.
For managing complex relations, check out the Backbone-Relational plugin, which has a lot of features and allows you to define relations in a declarative syntax.
One thing to watch out for is instantiating multiple instances of the same model. If you instantiate two copies of an "Issue" model with the same ID, and even the same data, it's very possible for them to get out of sync with each other. They are treated as two completely different objects, and if you call "set()" or "fetch()" on one, the other won't be updated. There are cases where this is fine and expected, but other times this may lead to confusing bugs.
You can get around this by being sure to pass around references to existing models rather than creating new ones everywhere. Another good approach is to use a shared Collection instance that gets passed around as a global "store" for retrieving model instances.
We've already created our issue model and parts of our repo model. Let's finish off the repo model and add the main "board" model also.
Here's the full repo model:
// models/repo.js
Repo.Model = Backbone.Model.extend({
initialize: function () {
this.issues = new app.Issue.Collection();
this.issues.repo = this;
// When this repo is destroyed, destroy its issues too.
this.on('destroy', function () {
_.invoke(this.issues.models.slice(), 'destroy');
});
},
url: function () {
return app.apiRoot + '/repos/' + this.get('full_name');
},
urlRoot: function () {
return app.apiRoot + '/users/' + this.get('owner').login + '/repos';
},
toBoard: function () {
var attrs = _.pick(this.attributes, 'id', 'name');
attrs.owner = {login: this.get('owner').login};
return attrs;
}
});
Again, we have the defaults, initialize, url and urlRoot methods. The initialize method creates a new issues collection for each repo. It also sets up an event handler for when the destroy event is triggered on itself. This event is triggered when the repo is destroyed (removed from the board and removed from local storage). When this happens any issues from the repo are also destroyed. Rather than looping through each of the issue models, we simply use underscore's invoke method to destroy all the issue models in the collection. We've already looked at the toBoard custom method.
Now lets build the main "board" model. This is a single model that will store which repos we are tracking on our board. We also use it to store whether we should keep showing the welcome modal view.
var Board = app.Board = app.Board || {};
Board.Model = Backbone.Model.extend({
defaults: {
showWelcome: true
},
urlRoot: '/boards',
initialize: function () {
this.repos = new app.Repo.Collection();
this.issues = new app.Issue.Collection();
this.on('change', function () { this.save(); });
var addIssue = function (issue) {this.add(issue)};
// Save the board when a repo is added or removed.
this.listenTo(this.repos, {
add: function (repo) {
this.issues.add(repo.issues.models);
this.issues.listenTo(repo.issues, 'add', addIssue);
this.save();
},
remove: function (repo) {
this.issues.stopListening(repo.issues, 'add', addIssue);
this.save();
}
});
// Save repos and issues when they are added or changed.
this.repos.on('add change', function (repo) { repo.save(); });
this.issues.on('add change', function (issue) { issue.save(); });
},
parse: function (res) {
this.repos.set(res.repos);
// **Don't** fetch on the collection (i.e. repos.fetch()), but
// individually as the repos collection spans many owners and the
// issues collection spans many repos.
this.repos.invoke('fetch');
_.invoke(_.pluck(this.repos.models, 'issues'), 'fetch');
delete res.repos;
return res;
},
toJSON: function () {
var attrs = _.clone(this.attributes);
attrs.repos = this.repos.invoke('toBoard');
return attrs;
}
});
There's quite a lot happening here, so let's go through each property.
By default the welcome box should be shown.
This gives our sync method the key to save the board data. If you open up your console and type "localstorage," you will see see all your board data stored with the key "/boards."
When the board is first instantiated an issue collection and a repo collection are both created.
We then set up some event handlers, to ensure that the board is saved whenever changes are made or when repos are added or removed.
Event handlers are also added to the repo and issue collection's change events to ensure that the underlying repos and issues are saved.
These methods work in tandem. The toJSON method defines what data should be persisted to local storage when the board is saved--any board properties plus the resuts of the "toBoard" method for each of the repos.
The parse method is called when this data is retrieved from local storage. It updates the repo collection (created in "initialize") with the repo metadata saved to local storage. It then ensures that these repos and any associated issues are fetched (i.e. populated with data, also from local storage). The parse method returns the input "res" without the repo's attribute. This means any other attributes on the board model can be saved and retrieved successfully.
Use instances of Backbone.Model to represent your business objects and use them to store data and call their appropriate methods (e.g., fetch(), destroy()) to sync with a server or other data source. By using Models to represent your data object, you have a consistent internal API that other parts of your application can rely on for storing and retrieving data.
We've built the three models used in the Hubbub application. To get a good idea of how they work, try accessing models in the console. For example, with the finished application loaded and a few repos already added, try the following:
app.board.issues.at(0);
// This will return the first issue model
app.board.repos.at(0).toBoard();
// Gets the first repo model and runs the "toBoard" method
app.board.toJSON();
// See what data is being saved to local storage for your board
Next up we will learn all about Collections in Backbone.
There has been error in communication with Booktype server. Not sure right now where is the problem.
You should refresh this page.