Backbone Basics - Developing Backbone.js Applications (2013)

Developing Backbone.js Applications (2013)

Chapter 3. Backbone Basics

In this section, you’ll learn the essentials of Backbone’s models, views, collections, events, and routers. This isn’t by any means a replacement for the official documentation, but it will help you understand many of the core concepts behind Backbone before you start building applications using it.

Getting Set Up

Before we dive into more code examples, let’s define some boilerplate markup you can use to specify the dependencies Backbone requires. This boilerplate can be reused in many ways with little to no alteration and will allow you to run code from examples with ease.

You can paste the following into your text editor of choice, replacing the commented line between the <script> tags with the JavaScript from any given example:

<!DOCTYPE HTML>

<html>

<head>

<meta charset="UTF-8">

<title>Title</title>

</head>

<body>

<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">

</script>

<script src="http://documentcloud.github.com/underscore/underscore-min.js">

</script>

<script src="http://documentcloud.github.com/backbone/backbone-min.js">

</script>

<script>

// Your code goes here

</script>

</body>

</html>

You can then save and run the file in your browser of choice, such as Chrome or Firefox. Alternatively, if you prefer working with an online code editor, jsFiddle and jsBin versions of this boilerplate are also available.

Most examples can also be run directly from within the console in your browser’s developer tools, assuming you’ve loaded the boilerplate HTML page so that Backbone and its dependencies are available for use.

For Chrome, you can open up the DevTools via the Chrome menu in the top-right corner: select Tools→Developer Tools or alternatively use the Ctrl+Shift+I shortcut on Windows/Linux or ⌘-Alt-I on Mac. See Figure 3-1.

The Chrome DevTools console

Figure 3-1. The Chrome DevTools console

Next, switch to the Console tab, from which you can enter and run any piece of JavaScript code by pressing the return key. You can also use the Console as a multiline editor using the Shift+Enter shortcut on Windows, or Ctrl-Enter shortcut on Mac, to move from the end of one line to the start of another.

Models

Backbone models contain data for an application as well as the logic around this data. For example, we can use a model to represent the concept of a todo item, including its attributes like title (todo content) and completed (current state of the todo).

We can create models by extending Backbone.Model as follows:

var Todo = Backbone.Model.extend({});

// We can then create our own concrete instance of a (Todo) model

// with no values at all:

var todo1 = new Todo();

// Following logs: {}

console.log(JSON.stringify(todo1));

// or with some arbitrary data:

var todo2 = new Todo({

title: 'Check the attributes of both model instances in the console.',

completed: true

});

// Following logs: {"title":"Check the attributes of both model

// instances in the console.","completed":true}

console.log(JSON.stringify(todo2));

Initialization

The initialize() method is called when a new instance of a model is created. Its use is optional; however, here you’ll see why it’s good practice to use it.

var Todo = Backbone.Model.extend({

initialize: function(){

console.log('This model has been initialized.');

}

});

var myTodo = new Todo();

// Logs: This model has been initialized.

Default values

There are times when you want your model to have a set of default values (e.g., in a scenario where a complete set of data isn’t provided by the user). You can set these using a property called defaults in your model.

var Todo = Backbone.Model.extend({

// Default todo attribute values

defaults: {

title: '',

completed: false

}

});

// Now we can create our concrete instance of the model

// with default values as follows:

var todo1 = new Todo();

// Following logs: {"title":"","completed":false}

console.log(JSON.stringify(todo1));

// Or we could instantiate it with some of the attributes (e.g., with

// custom title):

var todo2 = new Todo({

title: 'Check attributes of the logged models in the console.'

});

// Following logs: {"title":"Check attributes of the logged models

// in the console.","completed":false}

console.log(JSON.stringify(todo2));

// Or override all of the default attributes:

var todo3 = new Todo({

title: 'This todo is done, so take no action on this one.',

completed: true

});

// Following logs: {"title":"This todo is done, so take no action on

// this one.","completed":true}

console.log(JSON.stringify(todo3));

Getters and Setters

Model.get()

Model.get() provides easy access to a model’s attributes.

var Todo = Backbone.Model.extend({

// Default todo attribute values

defaults: {

title: '',

completed: false

}

});

var todo1 = new Todo();

console.log(todo1.get('title')); // empty string

console.log(todo1.get('completed')); // false

var todo2 = new Todo({

title: "Retrieved with model's get() method.",

completed: true

});

console.log(todo2.get('title')); // Retrieved with model's get() method.

console.log(todo2.get('completed')); // true

If you need to read or clone all of a model’s data attributes, use its toJSON() method. This method returns a copy of the attributes as an object (not a JSON string, despite its name). (When JSON.stringify() is passed an object with a toJSON() method, it stringifies the return value oftoJSON() instead of the original object. The examples in the previous section took advantage of this feature when they called JSON.stringify() to log model instances.)

var Todo = Backbone.Model.extend({

// Default todo attribute values

defaults: {

title: '',

completed: false

}

});

var todo1 = new Todo();

var todo1Attributes = todo1.toJSON();

// Following logs: {"title":"","completed":false}

console.log(todo1Attributes);

var todo2 = new Todo({

title: "Try these examples and check results in console.",

completed: true

});

// logs: {"title":"Try these examples and check results in console.",

// "completed":true}

console.log(todo2.toJSON());

Model.set()

Model.set() sets a hash containing one or more attributes on the model. When any of these attributes alter the state of the model, a change event is triggered on it. Change events for each attribute are also triggered and can be bound to the model (such as change:name, change:age).

var Todo = Backbone.Model.extend({

// Default todo attribute values

defaults: {

title: '',

completed: false

}

});

// Setting the value of attributes via instantiation

var myTodo = new Todo({

title: "Set through instantiation."

});

console.log('Todo title: ' + myTodo.get('title'));

// Todo title: Set through instantiation.

console.log('Completed: ' + myTodo.get('completed'));

// Completed: false

// Set single attribute value at a time through Model.set():

myTodo.set("title", "Title attribute set through Model.set().");

console.log('Todo title: ' + myTodo.get('title'));

// Todo title: Title attribute set through Model.set().

console.log('Completed: ' + myTodo.get('completed'));

// Completed: false

// Set map of attributes through Model.set():

myTodo.set({

title: "Both attributes set through Model.set().",

completed: true

});

console.log('Todo title: ' + myTodo.get('title'));

// Todo title: Both attributes set through Model.set().

console.log('Completed: ' + myTodo.get('completed'));

// Completed: true

Direct access

Models expose an .attributes attribute, which represents an internal hash containing the state of that model. This is generally in the form of a JSON object similar to the model data you might find on the server, but it can take other forms.

Setting values through the .attributes attribute on a model bypasses triggers bound to the model.

Passing {silent:true} on change doesn’t delay individual "change:attr" events; instead, they are silenced entirely:

var Person = new Backbone.Model();

Person.set({name: 'Jeremy'}, {silent: true});

console.log(!Person.hasChanged(0));

// true

console.log(!Person.hasChanged(''));

// true

Remember, where possible it is best practice to use Model.set(), or direct instantiation as explained earlier.

Listening for Changes to Your Model

If you want to receive a notification when a Backbone model changes, you can bind a listener to the model for its change event. A convenient place to add listeners is in the initialize() function, as shown here:

var Todo = Backbone.Model.extend({

// Default todo attribute values

defaults: {

title: '',

completed: false

},

initialize: function(){

console.log('This model has been initialized.');

this.on('change', function(){

console.log('- Values for this model have changed.');

});

}

});

var myTodo = new Todo();

myTodo.set('title', 'The listener is triggered whenever an attribute

// value changes.');

console.log('Title has changed: ' + myTodo.get('title'));

myTodo.set('completed', true);

console.log('Completed has changed: ' + myTodo.get('completed'));

myTodo.set({

title: 'Changing more than one attribute at the same time only triggers

// the listener once.',

completed: true

});

// Above logs:

// This model has been initialized.

// - Values for this model have changed.

// Title has changed: The listener is triggered when an attribute value changes.

// - Values for this model have changed.

// Completed has changed: true

// - Values for this model have changed.

You can also listen for changes to individual attributes in a Backbone model. In the following example, we log a message whenever a specific attribute (the title of our Todo model) is altered.

var Todo = Backbone.Model.extend({

// Default todo attribute values

defaults: {

title: '',

completed: false

},

initialize: function(){

console.log('This model has been initialized.');

this.on('change:title', function(){

console.log('Title value for this model has changed.');

});

},

setTitle: function(newTitle){

this.set({ title: newTitle });

}

});

var myTodo = new Todo();

// Both of the following changes trigger the listener:

myTodo.set('title', 'Check what\'s logged.');

myTodo.setTitle('Go fishing on Sunday.');

// But this change type is not observed, so no listener is triggered:

myTodo.set('completed', true);

console.log('Todo set as completed: ' + myTodo.get('completed'));

// Above logs:

// This model has been initialized.

// Title value for this model has changed.

// Title value for this model has changed.

// Todo set as completed: true

Validation

Backbone supports model validation through model.validate(), which allows checking the attribute values for a model prior to setting them. By default, validation occurs when the model is persisted via the save() method or when set() is called if {validate:true} is passed as an argument.

var Person = new Backbone.Model({name: 'Jeremy'});

// Validate the model name

Person.validate = function(attrs) {

if (!attrs.name) {

return 'I need your name';

}

};

// Change the name

Person.set({name: 'Samuel'});

console.log(Person.get('name'));

// 'Samuel'

// Remove the name attribute, force validation

Person.unset('name', {validate: true});

// false

We also make use of the unset() method, which removes an attribute by deleting it from the internal model attribute’s hash.

Validation functions can be as simple or complex as necessary. If the attributes provided are valid, nothing should be returned from .validate(). If they are invalid, an error value should be returned instead.

Should an error be returned:

§ An invalid event will be triggered, setting the validationError property on the model with the value that is returned by this method.

§ .save() will not continue and the attributes of the model will not be modified on the server.

Here is a more complete validation example:

var Todo = Backbone.Model.extend({

defaults: {

completed: false

},

validate: function(attribs){

if(attribs.title === undefined){

return "Remember to set a title for your todo.";

}

},

initialize: function(){

console.log('This model has been initialized.');

this.on("invalid", function(model, error){

console.log(error);

});

}

});

var myTodo = new Todo();

myTodo.set('completed', true, {validate: true});

// logs: Remember to set a title for your todo.

console.log('completed: ' + myTodo.get('completed')); // completed: false

NOTE

The attributes object passed to the validate function represents what the attributes would be after the current set() or save() completes. This object is distinct from the current attributes of the model and from the parameters passed to the operation. Since it is created by shallow copy, it is not possible to change any Number, String, or Boolean attribute of the input within the function, but it is possible to change attributes in nested objects.

An example of this (by @fivetanley) is available at http://jsfiddle.net/2NdDY/7/.

Views

Views in Backbone don’t contain the HTML markup for your application; they contain the logic behind the presentation of the model’s data to the user. They achieve this using JavaScript templating (for example, Underscore microtemplates, Mustache, jQuery-tmpl, and so on). A view’srender() method can be bound to a model’s change() event, enabling the view to instantly reflect model changes without requiring a full page refresh.

Creating New Views

Creating a new view is relatively straightforward and similar to creating new models. To create a new view, simply extend Backbone.View. We introduced the following sample TodoView in the previous chapter; now let’s take a closer look at how it works.

var TodoView = Backbone.View.extend({

tagName: 'li',

// Cache the template function for a single item.

todoTpl: _.template( "An example template" ),

events: {

'dblclick label': 'edit',

'keypress .edit': 'updateOnEnter',

'blur .edit': 'close'

},

// Rerender the titles of the todo item.

render: function() {

this.$el.html( this.todoTpl( this.model.toJSON() ) );

this.input = this.$('.edit');

return this;

},

edit: function() {

// executed when todo label is double-clicked

},

close: function() {

// executed when todo loses focus

},

updateOnEnter: function( e ) {

// executed on each keypress when in todo edit mode,

// but we'll wait for enter to get in action

}

});

var todoView = new TodoView();

// log reference to a DOM element that corresponds to the view instance

console.log(todoView.el); // logs <li></li>

What Is el?

The central property of a view is el (the value logged in the last statement of the example). What is el and how is it defined?

el is basically a reference to a DOM element, and all views must have one. Views can use el to compose their element’s content and then insert it into the DOM all at once, which makes for faster rendering because the browser performs the minimum required number of reflows and repaints.

There are two ways to associate a DOM element with a view: a new element can be created for the view and subsequently added to the DOM, or a reference can be made to an element that already exists in the page.

If you want to create a new element for your view, set any combination of the following properties on the view: tagName, id, and className. The framework will create a new element for you, and a reference to it will be available at the el property. If nothing is specified, tagNamedefaults to div.

In the preceding example, tagName is set to li, resulting in the creation of a li element. The following example creates a ul element with id and class attributes:

var TodosView = Backbone.View.extend({

tagName: 'ul', // required, but defaults to 'div' if not set

className: 'container', // optional, you can assign multiple classes to

// this property like so: 'container homepage'

id: 'todos', // optional

});

var todosView = new TodosView();

console.log(todosView.el); // logs <ul id="todos" class="container"></ul>

The preceding code creates the following DOM element but doesn’t append it to the DOM.

<ul id="todos" class="container"></ul>

If the element already exists in the page, you can set el as a CSS selector that matches the element.

el: '#footer'

Alternatively, you can set el to an existing element when creating the view:

var todosView = new TodosView({el: $('#footer')});

NOTE

When declaring a view, you can define options, el, tagName, id, and className as functions, if you want their values to be determined at runtime.

$el and $()

View logic often needs to invoke jQuery or Zepto functions on the el element and elements nested within it. Backbone makes it easy to do so by defining the $el property and $() function. The view.$el property is equivalent to $(view.el), and view.$(selector) is equivalent to$(view.el).find(selector). In our TodosView example’s render method, we see this.$el used to set the HTML of the element and this.$() used to find subelements of class edit.

setElement

If you need to apply an existing Backbone view to a different DOM element, you can use setElement. Overriding this.el needs to both change the DOM reference and rebind events to the new element (and unbind from the old).

setElement will create a cached $el reference for you, moving the delegated events for a view from the old element to the new one.

// We create two DOM elements representing buttons

// which could easily be containers or something else

var button1 = $('<button></button>');

var button2 = $('<button></button>');

// Define a new view

var View = Backbone.View.extend({

events: {

click: function(e) {

console.log(view.el === e.target);

}

}

});

// Create a new instance of the view, applying it

// to button1

var view = new View({el: button1});

// Apply the view to button2 using setElement

view.setElement(button2);

button1.trigger('click');

button2.trigger('click'); // returns true

The el property represents the markup portion of the view that will be rendered; to get the view to actually render to the page, you need to add it as a new element or append it to an existing element.

// We can also provide raw markup to setElement

// as follows (just to demonstrate it can be done):

var view = new Backbone.View;

view.setElement('<p><a><b>test</b></a></p>');

view.$('a b').html(); // outputs "test"

Understanding render()

render() is an optional function that defines the logic for rendering a template. We’ll use Underscore’s microtemplating in these examples, but remember you can use other templating frameworks if you prefer. Our example will reference the following HTML markup:

<!doctype html>

<html lang="en">

<head>

<meta charset="utf-8">

<title></title>

<meta name="description" content="">

</head>

<body>

<div id="todo">

</div>

<script type="text/template" id="item-template">

<div>

<input id="todo_complete" type="checkbox" <%= completed ?

'checked="checked"' : '' %>>

<%= title %>

</div>

</script>

<script src="underscore-min.js"></script>

<script src="backbone-min.js"></script>

<script src="jquery-min.js"></script>

<script src="example.js"></script>

</body>

</html>

The _.template method in Underscore compiles JavaScript templates into functions that can be evaluated for rendering. In the TodoView, I’m passing the markup from the template with an id of item-template to _.template() to be compiled and stored in the todoTpl property when the view is created.

The render() method uses this template by passing it the toJSON() encoding of the attributes of the model associated with the view. The template returns its markup after using the model’s title and completed flag to evaluate the expressions containing them. I then set this markup as the HTML content of the el DOM element using the $el property.

Presto! This populates the template, giving you a data-complete set of markup in just a few short lines of code.

A common Backbone convention is to return this at the end of render(). This is useful for a number of reasons, including:

§ Making views easily reusable in other parent views

§ Creating a list of elements without rendering and painting each of them individually, only to be drawn once the entire list is populated

Let’s try to implement the latter of these. The render method of a simple ListView that doesn’t use an ItemView for each item could be written as follows:

var ListView = Backbone.View.extend({

render: function(){

this.$el.html(this.model.toJSON());

}

});

Simple enough. Let’s now assume we’ve decided to construct the items using an ItemView to provide enhanced behavior to our list. The ItemView could be written like so:

var ItemView = Backbone.View.extend({

events: {},

render: function(){

this.$el.html(this.model.toJSON());

return this;

}

});

Note the usage of return this; at the end of render. This common pattern enables us to reuse the view as a subview. We can also use it to prerender the view prior to rendering. Doing so requires that we make a change to our ListView’s render method as follows:

var ListView = Backbone.View.extend({

render: function(){

// Assume our model exposes the items we will

// display in our list

var items = this.model.get('items');

// Loop through each of our items using the Underscore

// _.each iterator

_.each(items, function(item){

// Create a new instance of the ItemView, passing

// it a specific model item

var itemView = new ItemView({ model: item });

// The itemView's DOM element is appended after it

// has been rendered. Here, the 'return this' is helpful

// as the itemView renders its model. Later, we ask for

// its output ("el")

this.$el.append( itemView.render().el );

}, this);

}

});

The events hash

The Backbone events hash allows us to attach event listeners to either el-relative custom selectors, or directly to el if no selector is provided. An event takes the form of a key-value pair 'eventName selector': 'callbackFunction', and a number of DOM event types are supported, including click, submit, mouseover, dblclick, and more.

// A sample view

var TodoView = Backbone.View.extend({

tagName: 'li',

// with an events hash containing DOM events

// specific to an item:

events: {

'click .toggle': 'toggleCompleted',

'dblclick label': 'edit',

'click .destroy': 'clear',

'blur .edit': 'close'

},

What isn’t instantly obvious is that while Backbone uses jQuery’s .delegate() underneath, it goes further by extending it so that this always refers to the current view object within callback functions. The only thing to really keep in mind is that any string callback supplied to theevents attribute must have a corresponding function with the same name within the scope of your view.

The declarative, delegated jQuery events means that you don’t have to worry about whether a particular element has been rendered to the DOM yet or not. Usually with jQuery you have to worry about presence or absence in the DOM all the time when binding events.

In our TodoView example, the edit callback is invoked when the user double-clicks a label element within the el element, updateOnEnter is called for each keypress in an element with class edit, and close executes when an element with class edit loses focus. Each of these callback functions can use this to refer to the TodoView object.

Note that you can also bind methods yourself using _.bind(this.viewEvent, this), which is effectively what the value in each event’s key-value pair is doing. Here we use _.bind to rerender our view when a model changes:

var TodoView = Backbone.View.extend({

initialize: function() {

this.model.bind('change', _.bind(this.render, this));

}

});

_.bind works on only one method at a time, but it supports currying; because it returns the bound function, you can use _.bind on an anonymous function.

Collections

Collections are sets of models, and you create them by extending Backbone.Collection.

Normally, when creating a collection you’ll also want to define a property specifying the type of model that your collection will contain, along with any instance properties required.

In the following example, we create a TodoCollection that will contain our Todo models:

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var TodosCollection = Backbone.Collection.extend({

model: Todo

});

var myTodo = new Todo({title:'Read the whole book', id: 2});

// pass array of models on collection instantiation

var todos = new TodosCollection([myTodo]);

console.log("Collection size: " + todos.length); // Collection size: 1

Adding and Removing Models

The preceding example populated the collection using an array of models when it was instantiated. After a collection has been created, you can add and remove models using the add() and remove() methods:

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var TodosCollection = Backbone.Collection.extend({

model: Todo,

});

var a = new Todo({ title: 'Go to Jamaica.'}),

b = new Todo({ title: 'Go to China.'}),

c = new Todo({ title: 'Go to Disneyland.'});

var todos = new TodosCollection([a,b]);

console.log("Collection size: " + todos.length);

// Logs: Collection size: 2

todos.add(c);

console.log("Collection size: " + todos.length);

// Logs: Collection size: 3

todos.remove([a,b]);

console.log("Collection size: " + todos.length);

// Logs: Collection size: 1

todos.remove(c);

console.log("Collection size: " + todos.length);

// Logs: Collection size: 0

Note that add() and remove() accept both individual models and lists of models.

Also note that when you are using add() on a collection, passing {merge: true} causes duplicate models to have their attributes merged into the existing models, instead of being ignored.

var items = new Backbone.Collection;

items.add([{ id : 1, name: "Dog" , age: 3}, { id : 2, name: "cat" , age: 2}]);

items.add([{ id : 1, name: "Bear" }], {merge: true });

items.add([{ id : 2, name: "lion" }]); // merge: false

console.log(JSON.stringify(items.toJSON()));

// [{"id":1,"name":"Bear","age":3},{"id":2,"name":"cat","age":2}]

Retrieving Models

There are a few different ways to retrieve a model from a collection. The most straightforward is to use Collection.get(), which accepts a single id as follows:

var myTodo = new Todo({title:'Read the whole book', id: 2});

// pass array of models on collection instantiation

var todos = new TodosCollection([myTodo]);

var todo2 = todos.get(2);

// Models, as objects, are passed by reference

console.log(todo2 === myTodo); // true

In client-server applications, collections contain models obtained from the server. Anytime you’re exchanging data between the client and a server, you will need a way to uniquely identify models. In Backbone, you do so using the id, cid, and idAttribute properties.

Each model in Backbone has an id, which is a unique identifier that is either an integer or string (for example, a UUID). Models also have a cid (client ID) which is automatically generated by Backbone when the model is created. Either identifier can be used to retrieve a model from a collection.

The main difference between them is that the cid is generated by Backbone, which is helpful when you don’t have a true id; this may be the case if your model has yet to be saved to the server or you aren’t saving it to a database.

The idAttribute is the identifying attribute of the model returned from the server (i.e., the id in your database). This tells Backbone which data field from the server should be used to populate the id property (think of it as a mapper). By default, it assumes id, but this can be customized as needed. For instance, if your server sets a unique attribute on your model named userId, then you would set idAttribute to userId in your model definition.

The value of a model’s idAttribute should be set by the server when the model is saved. After this point, you shouldn’t need to set it manually, unless further control is required.

Internally, Backbone.Collection contains an array of models enumerated by their id property, if the model instances happen to have one. When collection.get(id) is called, this array is checked for the existence of the model instance with the corresponding id.

// extends the previous example

var todoCid = todos.get(todo2.cid);

// As mentioned in previous example,

// models are passed by reference

console.log(todoCid === myTodo); // true

Listening for Events

Because collections represent a group of items, we can listen for add and remove events, which occur when models are added to or removed from a collection. Here’s an example:

var TodosCollection = new Backbone.Collection();

TodosCollection.on("add", function(todo) {

console.log("I should " + todo.get("title") + ". Have I done it before? "

+ (todo.get("completed") ? 'Yeah!': 'No.' ));

});

TodosCollection.add([

{ title: 'go to Jamaica', completed: false },

{ title: 'go to China', completed: false },

{ title: 'go to Disneyland', completed: true }

]);

// The above logs:

// I should go to Jamaica. Have I done it before? No.

// I should go to China. Have I done it before? No.

// I should go to Disneyland. Have I done it before? Yeah!

In addition, we’re also able to bind to a change event to listen for changes to any of the models in the collection.

var TodosCollection = new Backbone.Collection();

// log a message if a model in the collection changes

TodosCollection.on("change:title", function(model) {

console.log("Changed my mind! I should " + model.get('title'));

});

TodosCollection.add([

{ title: 'go to Jamaica.', completed: false, id: 3 },

]);

var myTodo = TodosCollection.get(3);

myTodo.set('title', 'go fishing');

// Logs: Changed my mind! I should go fishing

jQuery-style event maps of the form obj.on({click: action}) can also be used. These can be clearer than using three separate calls to .on, and should align better with the events hash used in views:

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var myTodo = new Todo();

myTodo.set({title: 'Buy some cookies', completed: true});

myTodo.on({

'change:title' : titleChanged,

'change:completed' : stateChanged

});

function titleChanged(){

console.log('The title was changed!');

}

function stateChanged(){

console.log('The state was changed!');

}

myTodo.set({title: 'Get the groceries'});

// The title was changed!

Backbone events also support a once() method, which ensures that a callback fires only one time when a notification arrives. It is similar to Node’s once, or jQuery’s one. This is particularly useful for when you want to say, “The next time something happens, do this.”

// Define an object with two counters

var TodoCounter = { counterA: 0, counterB: 0 };

// Mix in Backbone Events

_.extend(TodoCounter, Backbone.Events);

// Increment counterA, triggering an event

var incrA = function(){

TodoCounter.counterA += 1;

TodoCounter.trigger('event');

};

// Increment counterB

var incrB = function(){

TodoCounter.counterB += 1;

};

// Use once rather than having to explicitly unbind

// our event listener

TodoCounter.once('event', incrA);

TodoCounter.once('event', incrB);

// Trigger the event once again

TodoCounter.trigger('event');

// Check our output

console.log(TodoCounter.counterA === 1); // true

console.log(TodoCounter.counterB === 1); // true

counterA and counterB should have been incremented only once.

Resetting/Refreshing Collections

Rather than adding or removing models individually, you might want to update an entire collection at once. Collection.set() takes an array of models and performs the necessary add, remove, and change operations required to update the collection.

var TodosCollection = new Backbone.Collection();

TodosCollection.add([

{ id: 1, title: 'go to Jamaica.', completed: false },

{ id: 2, title: 'go to China.', completed: false },

{ id: 3, title: 'go to Disneyland.', completed: true }

]);

// we can listen for add/change/remove events

TodosCollection.on("add", function(model) {

console.log("Added " + model.get('title'));

});

TodosCollection.on("remove", function(model) {

console.log("Removed " + model.get('title'));

});

TodosCollection.on("change:completed", function(model) {

console.log("Completed " + model.get('title'));

});

TodosCollection.set([

{ id: 1, title: 'go to Jamaica.', completed: true },

{ id: 2, title: 'go to China.', completed: false },

{ id: 4, title: 'go to Disney World.', completed: false }

]);

// Above logs:

// Removed go to Disneyland.

// Completed go to Jamaica.

// Added go to Disney World.

If you need to simply replace the entire content of the collection, then you can use Collection.reset() as follows:

var TodosCollection = new Backbone.Collection();

// we can listen for reset events

TodosCollection.on("reset", function() {

console.log("Collection reset.");

});

TodosCollection.add([

{ title: 'go to Jamaica.', completed: false },

{ title: 'go to China.', completed: false },

{ title: 'go to Disneyland.', completed: true }

]);

console.log('Collection size: ' + TodosCollection.length); // Collection size: 3

TodosCollection.reset([

{ title: 'go to Cuba.', completed: false }

]);

// Above logs 'Collection reset.'

console.log('Collection size: ' + TodosCollection.length); // Collection size: 1

Another useful tip is to use reset with no arguments to clear out a collection completely. This is handy when you’re dynamically loading a new page of results where you want to blank out the current page of results.

myCollection.reset();

Note that using Collection.reset() doesn’t fire any add or remove events. A reset event is fired instead, as shown in the previous example. The reason you might want to use this is to perform super-optimized rendering in extreme cases where individual events are too expensive.

Also note that when you’re listening to a reset event, the list of previous models is available in options.previousModels, for convenience.

var Todo = new Backbone.Model();

var Todos = new Backbone.Collection([Todo])

.on('reset', function(Todos, options) {

console.log(options.previousModels);

console.log([Todo]);

console.log(options.previousModels[0] === Todo); // true

});

Todos.reset([]);

An update() method is available for collections (and is also available as an option to fetch) for smart updating of sets of models. This method attempts to perform smart updating of a collection using a specified list of models. When a model in this list isn’t present in the collection, it is added. If it is present, its attributes will be merged. Models that are present in the collection but not in the list are removed.

var theBeatles = new Collection(['john', 'paul', 'george', 'ringo']);

theBeatles.update(['john', 'paul', 'george', 'pete']);

// Fires a `remove` event for 'ringo', and an `add` event for 'pete'.

// Updates any of john, paul, and george's attributes that may have

// changed over the years.

Underscore Utility Functions

Backbone takes full advantage of its hard dependency on Underscore by making many of its utilities directly available on collections.

forEach: Iterate over collections

var Todos = new Backbone.Collection();

Todos.add([

{ title: 'go to Belgium.', completed: false },

{ title: 'go to China.', completed: false },

{ title: 'go to Austria.', completed: true }

]);

// iterate over models in the collection

Todos.forEach(function(model){

console.log(model.get('title'));

});

// Above logs:

// go to Belgium.

// go to China.

// go to Austria.

sortBy(): Sort a collection on a specific attribute

// sort collection

var sortedByAlphabet = Todos.sortBy(function (todo) {

return todo.get("title").toLowerCase();

});

console.log("- Now sorted: ");

sortedByAlphabet.forEach(function(model){

console.log(model.get('title'));

});

// Above logs:

// go to Austria.

// go to Belgium.

// go to China.

map(): Create a new collection by mapping each value in a list through a transformation function

var count = 1;

console.log(Todos.map(function(model){

return count++ + ". " + model.get('title');

}));

// Above logs:

//1. go to Belgium.

//2. go to China.

//3. go to Austria.

min()/max(): Retrieve item with the min or max value of an attribute

Todos.max(function(model){

return model.id;

}).id;

Todos.min(function(model){

return model.id;

}).id;

pluck(): Extract a specific attribute

var captions = Todos.pluck('caption');

// returns list of captions

filter(): Filter a collection

Filter by an array of model IDs.

var Todos = Backbone.Collection.extend({

model: Todo,

filterById: function(ids){

return this.models.filter(

function(c) {

return _.contains(ids, c.id);

})

}

});

indexOf(): Return the item at a particular index within a collection

var People = new Backbone.Collection;

People.comparator = function(a, b) {

return a.get('name') < b.get('name') ? -1 : 1;

};

var tom = new Backbone.Model({name: 'Tom'});

var rob = new Backbone.Model({name: 'Rob'});

var tim = new Backbone.Model({name: 'Tim'});

People.add(tom);

People.add(rob);

People.add(tim);

console.log(People.indexOf(rob) === 0); // true

console.log(People.indexOf(tim) === 1); // true

console.log(People.indexOf(tom) === 2); // true

any() : Confirm if any of the values in a collection pass an iterator truth test

Todos.any(function(model){

return model.id === 100;

});

// or

Todos.some(function(model){

return model.id === 100;

});

size(): Return the size of a collection

Todos.size();

// equivalent to

Todos.length;

isEmpty(): Determine whether a collection is empty

var isEmpty = Todos.isEmpty();

groupBy(): Group a collection into groups like items

var Todos = new Backbone.Collection();

Todos.add([

{ title: 'go to Belgium.', completed: false },

{ title: 'go to China.', completed: false },

{ title: 'go to Austria.', completed: true }

]);

// create groups of completed and incomplete models

var byCompleted = Todos.groupBy('completed');

var completed = new Backbone.Collection(byCompleted[true]);

console.log(completed.pluck('title'));

// logs: ["go to Austria."]

In addition, several of the Underscore operations on objects are available as methods on models.

pick(): Extract a set of attributes from a model

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var todo = new Todo({title: 'go to Austria.'});

console.log(todo.pick('title'));

// logs {title: "go to Austria"}

omit(): Extract all attributes from a model except those listed

var todo = new Todo({title: 'go to Austria.'});

console.log(todo.omit('title'));

// logs {completed: false}

keys() and values(): Get lists of attribute names and values

var todo = new Todo({title: 'go to Austria.'});

console.log(todo.keys());

// logs: ["title", "completed"]

console.log(todo.values());

//logs: ["go to Austria.", false]

pairs(): Get list of attributes as [key, value] pairs

var todo = new Todo({title: 'go to Austria.'});

var pairs = todo.pairs();

console.log(pairs[0]);

// logs: ["title", "go to Austria."]

console.log(pairs[1]);

// logs: ["completed", false]

invert(): Create object in which the values are keys and the attributes are values

var todo = new Todo({title: 'go to Austria.'});

console.log(todo.invert());

// logs: {go to Austria.: "title", false: "completed"}

You can find the complete list of what Underscore can do in its official docs.

Chainable API

Speaking of utility methods, another bit of sugar in Backbone is its support for Underscore’s chain() method. Chaining is a common idiom in object-oriented languages; a chain is a sequence of method calls on the same object that are performed in a single statement. While Backbone makes Underscore’s array manipulation operations available as methods of collection objects, they cannot be directly chained since they return arrays rather than the original collection.

Fortunately, the inclusion of Underscore’s chain() method enables you to chain calls to these methods on collections.

The chain() method returns an object that has all of the Underscore array operations attached as methods that return that object. The chain ends with a call to the value() method, which simply returns the resulting array value. In case you haven’t seen it before, the chainable API looks like this:

var collection = new Backbone.Collection([

{ name: 'Tim', age: 5 },

{ name: 'Ida', age: 26 },

{ name: 'Rob', age: 55 }

]);

var filteredNames = collection.chain()

// start chain, returns wrapper around collection's models

.filter(function(item) { return item.get('age') > 10; })

// returns wrapped array excluding Tim

.map(function(item) { return item.get('name'); })

// returns wrapped array containing remaining names

.value(); // terminates the chain and returns the resulting array

console.log(filteredNames); // logs: ['Ida', 'Rob']

Some of the Backbone-specific methods do return this, which means they can be chained as well:

var collection = new Backbone.Collection();

collection

.add({ name: 'John', age: 23 })

.add({ name: 'Harry', age: 33 })

.add({ name: 'Steve', age: 41 });

var names = collection.pluck('name');

console.log(names); // logs: ['John', 'Harry', 'Steve']

RESTful Persistence

Thus far, all of our example data has been created in the browser. For most single-page applications, the models are derived from a data set residing on a server. This is an area in which Backbone dramatically simplifies the code you need to write to perform RESTful synchronization with a server through a simple API on its models and collections.

Fetching Models from the Server

Collections.fetch() retrieves a set of models from the server in the form of a JSON array by sending an HTTP GET request to the URL specified by the collection’s url property (which may be a function). When this data is received, a set() will be executed to update the collection.

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var TodosCollection = Backbone.Collection.extend({

model: Todo,

url: '/todos'

});

var todos = new TodosCollection();

todos.fetch(); // sends HTTP GET to /todos

Saving Models to the Server

While Backbone can retrieve an entire collection of models from the server at once, updates to models are performed individually via the model’s save() method. When save() is called on a model that was fetched from the server, it constructs a URL by appending the model’s id to the collection’s URL and sends an HTTP PUT to the server. If the model is a new instance that was created in the browser (it doesn’t have an id), then an HTTP POST is sent to the collection’s URL. Collections.create() can be used to create a new model, add it to the collection, and send it to the server in a single method call.

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var TodosCollection = Backbone.Collection.extend({

model: Todo,

url: '/todos'

});

var todos = new TodosCollection();

todos.fetch();

var todo2 = todos.get(2);

todo2.set('title', 'go fishing');

todo2.save(); // sends HTTP PUT to /todos/2

todos.create({title: 'Try out code samples'});

// sends HTTP POST to /todos and adds to collection

As mentioned earlier, a model’s validate() method is called automatically by save() and will trigger an invalid event on the model if validation fails.

Deleting Models from the Server

You can remove a model from the containing collection and the server by calling its destroy() method. Unlike Collection.remove(), which only removes a model from a collection, Model.destroy() will also send an HTTP DELETE to the collection’s URL.

var Todo = Backbone.Model.extend({

defaults: {

title: '',

completed: false

}

});

var TodosCollection = Backbone.Collection.extend({

model: Todo,

url: '/todos'

});

var todos = new TodosCollection();

todos.fetch();

var todo2 = todos.get(2);

todo2.destroy(); // sends HTTP DELETE to /todos/2 and removes from collection

Calling destroy on a model will return false if the model isNew:

var Todo = new Backbone.Model();

console.log(Todo.destroy());

// false

Options

Each RESTful API method accepts a variety of options. Most importantly, all methods accept success and error callbacks that can be used to customize the handling of server responses.

Specifying the {patch: true} option to Model.save() will cause it to use HTTP PATCH to send only the changed attributes (such as partial updates) to the server instead of the entire model—that is model.save(attrs, {patch: true}):

// Save partial using PATCH

model.clear().set({id: 1, a: 1, b: 2, c: 3, d: 4});

model.save();

model.save({b: 2, d: 4}, {patch: true});

console.log(this.syncArgs.method);

// 'patch'

Similarly, passing the {reset: true} option to Collection.fetch() will result in the collection being updated using reset() rather than set().

See the Backbone.js documentation for full descriptions of the supported options.

Events

Events are a basic inversion of control. Instead of having one function call another by name, the second function is registered as a handler to be called when a specific event occurs.

The part of your application that has to know how to call the other part of your app has been inverted. This is the core component that makes it possible for your business logic to not have to know about how your user interface works, and the most powerful thing about the Backbone events system.

Mastering events is one of the quickest ways to become more productive with Backbone, so let’s take a closer look at Backbone’s event model.

Backbone.Events is mixed into the other Backbone classes, including:

§ Backbone

§ Backbone.Model

§ Backbone.Collection

§ Backbone.Router

§ Backbone.History

§ Backbone.View

Note that Backbone.Events is mixed into the Backbone object. Since Backbone is globally visible, it can be used as a simple event bus:

Backbone.on('event', function() {

console.log('Handled Backbone event');

});

on(), off(), and trigger()

Backbone.Events can give any object the ability to bind and trigger custom events. We can mix this module into any object easily, and there isn’t a requirement for events to be declared before being bound to a callback handler. For example:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

// Add a custom event

ourObject.on('dance', function(msg){

console.log('We triggered ' + msg);

});

// Trigger the custom event

ourObject.trigger('dance', 'our event');

If you’re familiar with jQuery custom events or the concept of publish/subscribe, Backbone.Events provides a very similar system, with on being analogous to subscribe and trigger being similar to publish.

on binds a callback function to an object, as we’ve done with dance in the preceding example. The callback is invoked whenever the event is triggered.

The official Backbone.js documentation recommends namespacing event names using colons if you end up having quite a few of these on your page. For example:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

function dancing (msg) { console.log("We started " + msg); }

// Add namespaced custom events

ourObject.on("dance:tap", dancing);

ourObject.on("dance:break", dancing);

// Trigger the custom events

ourObject.trigger("dance:tap", "tap dancing. Yeah!");

ourObject.trigger("dance:break", "break dancing. Yeah!");

// This one triggers nothing as no listener listens for it

ourObject.trigger("dance", "break dancing. Yeah!");

A special all event is made available in case you would like notifications for every event that occurs on the object (for example, if you would like to screen events in a single location). The all event can be used as follows:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

function dancing (msg) { console.log("We started " + msg); }

ourObject.on("all", function(eventName){

console.log("The name of the event passed was " + eventName);

});

// This time each event will be caught with a catch 'all' event listener

ourObject.trigger("dance:tap", "tap dancing. Yeah!");

ourObject.trigger("dance:break", "break dancing. Yeah!");

ourObject.trigger("dance", "break dancing. Yeah!");

off removes callback functions that were previously bound to an object. Going back to our publish/subscribe comparison, think of it as an unsubscribe for custom events.

To remove the dance event we previously bound to ourObject, we would simply do:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

function dancing (msg) { console.log("We " + msg); }

// Add namespaced custom events

ourObject.on("dance:tap", dancing);

ourObject.on("dance:break", dancing);

// Trigger the custom events. Each will be caught and acted upon.

ourObject.trigger("dance:tap", "started tap dancing. Yeah!");

ourObject.trigger("dance:break", "started break dancing. Yeah!");

// Removes event bound to the object

ourObject.off("dance:tap");

// Trigger the custom events again, but one is logged.

ourObject.trigger("dance:tap", "stopped tap dancing.");

// won't be logged as it's not listened for

ourObject.trigger("dance:break", "break dancing. Yeah!");

To remove all callbacks for the event, we pass an event name (such as move) to the off() method on the object the event is bound to. If we wish to remove a specific callback, we can pass that callback as the second parameter:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

function dancing (msg) { console.log("We are dancing. " + msg); }

function jumping (msg) { console.log("We are jumping. " + msg); }

// Add two listeners to the same event

ourObject.on("move", dancing);

ourObject.on("move", jumping);

// Trigger the events. Both listeners are called.

ourObject.trigger("move", "Yeah!");

// Removes specified listener

ourObject.off("move", dancing);

// Trigger the events again. One listener left.

ourObject.trigger("move", "Yeah, jump, jump!");

Finally, as we have seen in our previous examples, trigger triggers a callback for a specified event (or a space-separated list of events). For example:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

function doAction (msg) { console.log("We are " + msg); }

// Add event listeners

ourObject.on("dance", doAction);

ourObject.on("jump", doAction);

ourObject.on("skip", doAction);

// Single event

ourObject.trigger("dance", 'just dancing.');

// Multiple events

ourObject.trigger("dance jump skip", 'very tired from so much action.');

trigger can pass multiple arguments to the callback function:

var ourObject = {};

// Mixin

_.extend(ourObject, Backbone.Events);

function doAction (action, duration) {

console.log("We are " + action + ' for ' + duration );

}

// Add event listeners

ourObject.on("dance", doAction);

ourObject.on("jump", doAction);

ourObject.on("skip", doAction);

// Passing multiple arguments to single event

ourObject.trigger("dance", 'dancing', "5 minutes");

// Passing multiple arguments to multiple events

ourObject.trigger("dance jump skip", 'on fire', "15 minutes");

listenTo() and stopListening()

While on() and off() add callbacks directly to an observed object, listenTo() tells an object to listen for events on another object, allowing the listener to keep track of the events for which it is listening. stopListening() can subsequently be called on the listener to tell it to stop listening for events:

var a = _.extend({}, Backbone.Events);

var b = _.extend({}, Backbone.Events);

var c = _.extend({}, Backbone.Events);

// add listeners to A for events on B and C

a.listenTo(b, 'anything', function(event){

console.log("anything happened"); });

a.listenTo(c, 'everything', function(event){

console.log("everything happened"); });

// trigger an event

b.trigger('anything'); // logs: anything happened

// stop listening

a.stopListening();

// A does not receive these events

b.trigger('anything');

c.trigger('everything');

stopListening() can also be used to selectively stop listening based on the event, model, or callback handler.

If you use on and off and remove views and their corresponding models at the same time, there are generally no problems. But a problem arises when you remove a view that had registered to be notified about events on a model, but you don’t remove the model or call off to remove the view’s event handler. Since the model has a reference to the view’s callback function, the JavaScript garbage collector cannot remove the view from memory. This is called a ghost view and is a common form of memory leak since the models generally tend to outlive the corresponding views during an application’s lifecycle. For details on the topic and a solution, check out this excellent article by Derick Bailey.

Practically, every on called on an object also requires an off to be called in order for the garbage collector to do its job. listenTo() changes that, allowing views to bind to model notifications and unbind from all of them with just one call: stopListening().

The default implementation of View.remove() makes a call to stopListening(), ensuring that any listeners bound via listenTo() are unbound before the view is destroyed.

var view = new Backbone.View();

var b = _.extend({}, Backbone.Events);

view.listenTo(b, 'all', function(){ console.log(true); });

b.trigger ('anything'); // logs: true

view.listenTo(b, 'all', function(){ console.log(false); });

view.remove(); // stopListening() implicitly called

b.trigger('anything');

// does not log anything

Events and Views

Within a view, there are two types of events you can listen for: DOM events and events triggered using the Event API. It is important to understand the differences in how views bind to these events and the context in which their callbacks are invoked.

You can bind DOM events using the view’s events property or using jQuery.on(). Within callbacks bound using the events property, this refers to the view object; any callbacks bound directly using jQuery, however, will have this set to the handling DOM element by jQuery. All DOM event callbacks are passed an event object by jQuery. See delegateEvents() in the Backbone documentation for additional details.

Event API events are bound as described in this section. If you bind the event using on() on the observed object, you can pass a context parameter as the third argument. If you bind the event using listenTo(), then within the callback this refers to the listener. The arguments passed to Event API callbacks depend on the type of event. See the Catalog of Events in the Backbone documentation for details.

The following example illustrates these differences:

<div id="todo">

<input type='checkbox' />

</div>

var View = Backbone.View.extend({

el: '#todo',

// bind to DOM event using events property

events: {

'click [type="checkbox"]': 'clicked',

},

initialize: function () {

// bind to DOM event using jQuery

this.$el.click(this.jqueryClicked);

// bind to API event

this.on('apiEvent', this.callback);

},

// 'this' is view

clicked: function(event) {

console.log("events handler for " + this.el.outerHTML);

this.trigger('apiEvent', event.type);

},

// 'this' is handling DOM element

jqueryClicked: function(event) {

console.log("jQuery handler for " + this.outerHTML);

},

callback: function(eventType) {

console.log("event type was " + eventType);

}

});

var view = new View();

Routers

In Backbone, routers provide a way for you to connect URLs (either hash fragments, or real) to parts of your application. Any piece of your application that you want to be bookmarkable, shareable, and back-button-able needs a URL.

Here are some examples of routes using the hash mark:

http://example.com/#about

http://example.com/#search/seasonal-horns/page2

An application will usually have at least one route mapping a URL route to a function that determines what happens when a user reaches that route. This relationship is defined as follows:

'route' : 'mappedFunction'

Let’s define our first router by extending Backbone.Router. For the purposes of this guide, we’re going to continue pretending we’re creating a complex todo application (something like a personal organizer/planner) that requires a complex TodoRouter.

Note the inline comments in the following code example, as they continue our lesson on routers.

var TodoRouter = Backbone.Router.extend({

/* define the route and function maps for this router */

routes: {

"about" : "showAbout",

/* Sample usage: http://example.com/#about */

"todo/:id" : "getTodo",

/* This is an example of using a ":param" variable, which allows us to

match any of the components between two URL slashes */

/* Sample usage: http://example.com/#todo/5 */

"search/:query" : "searchTodos",

/* We can also define multiple routes that are bound to the same map

function, in this case searchTodos(). Note below how we're optionally

passing in a reference to a page number if one is supplied */

/* Sample usage: http://example.com/#search/job */

"search/:query/p:page" : "searchTodos",

/* As we can see, URLs may contain as many ":param"s as we wish */

/* Sample usage: http://example.com/#search/job/p1 */

"todos/:id/download/*documentPath" : "downloadDocument",

/* This is an example of using a *splat. Splats are able to match

any number of URL components and can be combined with ":param"s*/

/* Sample usage: http://example.com/#todos/5/download/todos.doc */

/* If you wish to use splats for anything beyond default routing,

it's probably a good idea to leave them at the end of a URL;

otherwise, you may need to apply regular expression parsing

on your fragment */

"*other" : "defaultRoute"

/* This is a default route that also uses a *splat. Consider the

default route a wildcard for URLs that are either not matched or where

the user has incorrectly typed in a route path manually */

/* Sample usage: http://example.com/# <anything> */,

"optional(/:item)": "optionalItem",

"named/optional/(y:z)": "namedOptionalItem"

/* Router URLs also support optional parts via parentheses, without

having to use a regex. */

},

showAbout: function(){

},

getTodo: function(id){

/*

Note that the id matched in the above route will be passed to this

function */

console.log("You are trying to reach todo " + id);

},

searchTodos: function(query, page){

var page_number = page || 1;

console.log("Page number: " + page_number + " of the results for todos

containing the word: " + query);

},

downloadDocument: function(id, path){

},

defaultRoute: function(other){

console.log('Invalid. You attempted to reach:' + other);

}

});

/* Now that we have a router setup, we need to instantiate it */

var myTodoRouter = new TodoRouter();

Backbone offers an opt-in for HTML5 pushState support via window.history.pushState. This permits you to define routes such as http://backbonejs.org/just/an/example. This will be supported with automatic degradation when a user’s browser doesn’t support pushState. Note that it is vastly preferred if you’re capable of also supporting pushState on the server side, although it is a little more difficult to implement.

NOTE

You might be wondering if there’s a limit to the number of routers you should be using. Andrew de Andrade has pointed out that DocumentCloud, the creator of Backbone, usually only uses a single router in most of its applications. You’re very likely to not require more than one or two routers in your own projects; the majority of your application routing can be kept organized in a single router without it getting unwieldy.

Backbone.history

Next, we need to initialize Backbone.history, as it handles hashchange events in our application. This will automatically handle routes that have been defined and trigger callbacks when they’ve been accessed.

The Backbone.history.start() method will simply tell Backbone that it’s OK to begin monitoring all hashchange events as follows:

var TodoRouter = Backbone.Router.extend({

/* define the route and function maps for this router */

routes: {

"about" : "showAbout",

"search/:query" : "searchTodos",

"search/:query/p:page" : "searchTodos"

},

showAbout: function(){},

searchTodos: function(query, page){

var page_number = page || 1;

console.log("Page number: " + page_number + " of the results for todos

containing the word: " + query);

}

});

var myTodoRouter = new TodoRouter();

Backbone.history.start();

// Go to and check console:

// http://localhost/#search/job/p3 logs: Page number: 3 of the results for

// todos containing the word: job

// http://localhost/#search/job logs: Page number: 1 of the results for

// todos containing the word: job

// etc.

NOTE

To run the preceding example, you’ll need to create a local development environment and test project, which we will cover in Chapter 4.

If you would like to update the URL to reflect the application state at a particular point, you can use the router’s .navigate() method. By default, it simply updates your URL fragment without triggering the hashchange event:

// Let's imagine we would like a specific fragment

// (edit) once a user opens a single todo

var TodoRouter = Backbone.Router.extend({

routes: {

"todo/:id": "viewTodo",

"todo/:id/edit": "editTodo"

// ... other routes

},

viewTodo: function(id){

console.log("View todo requested.");

this.navigate("todo/" + id + '/edit');

// updates the fragment for us, but doesn't trigger the route

},

editTodo: function(id) {

console.log("Edit todo opened.");

}

});

var myTodoRouter = new TodoRouter();

Backbone.history.start();

// Go to: http://localhost/#todo/4

//

// URL is updated to: http://localhost/#todo/4/edit

// but editTodo() function is not invoked even though location we end up

// is mapped to it.

//

// logs: View todo requested.

It is also possible for Router.navigate() to trigger the route along with updating the URL fragment by passing the trigger:true option.

NOTE

This usage is discouraged. The recommended usage is the one previously described that creates a bookmarkable URL when your application transitions to a specific state.

var TodoRouter = Backbone.Router.extend({

routes: {

"todo/:id": "viewTodo",

"todo/:id/edit": "editTodo"

// ... other routes

},

viewTodo: function(id){

console.log("View todo requested.");

this.navigate("todo/" + id + '/edit', {trigger: true});

// updates the fragment and triggers the route as well

},

editTodo: function(id) {

console.log("Edit todo opened.");

}

});

var myTodoRouter = new TodoRouter();

Backbone.history.start();

// Go to: http://localhost/#todo/4

//

// URL is updated to: http://localhost/#todo/4/edit

// and this time editTodo() function is invoked.

//

// logs:

// View todo requested.

// Edit todo opened.

A route event is also triggered on the router in addition to being fired on Backbone.history.

Backbone.history.on('route', onRoute);

// Trigger 'route' event on router instance."

router.on('route', function(name, args) {

console.log(name === 'routeEvent');

});

location.replace('http://example.com#route-event/x');

Backbone.history.checkUrl();

Backbone’s Sync API

We previously discussed how Backbone supports RESTful persistence via the fetch(), save(), and destroy() methods on models, and the fetch() and create() methods on collections. Now we are going to take a closer look at Backbone’s sync method, which underlies these operations.

The Backbone.sync method is an integral part of Backbone.js. It assumes a jQuery-like $.ajax() method, so HTTP parameters are organized based on jQuery’s API. Since some legacy servers may not support JSON-formatted requests and HTTP PUT and DELETE operations, we can configure Backbone to emulate these capabilities using the two configuration variables shown here with their default values:

Backbone.emulateHTTP = false;

// set to true if server cannot handle HTTP PUT or HTTP DELETE

Backbone.emulateJSON = false;

// set to true if server cannot handle application/json requests

The inline Backbone.emulateHTTP option should be set to true if extended HTTP methods are not supported by the server. The Backbone.emulateJSON option should be set to true if the server does not understand the MIME type for JSON.

// Create a new library collection

var Library = Backbone.Collection.extend({

url : function() { return '/library'; }

});

// Define attributes for our model

var attrs = {

title : "The Tempest",

author : "Bill Shakespeare",

length : 123

};

// Create a new library instance

var library = new Library;

// Create a new instance of a model within our collection

library.create(attrs, {wait: false});

// Update with just emulateHTTP

library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}, {

emulateHTTP: true

});

// Check the ajaxSettings being used for our request

console.log(this.ajaxSettings.url === '/library/2-the-tempest');

// true

console.log(this.ajaxSettings.type === 'POST'); // true

console.log(this.ajaxSettings.contentType === 'application/json');

// true

// Parse the data for the request to confirm it is as expected

var data = JSON.parse(this.ajaxSettings.data);

console.log(data.id === '2-the-tempest'); // true

console.log(data.author === 'Tim Shakespeare'); // true

console.log(data.length === 123); // true

Similarly, we could just update using emulateJSON:

library.first().save({id: '2-the-tempest', author: 'Tim Shakespeare'}, {

emulateJSON: true

});

console.log(this.ajaxSettings.url === '/library/2-the-tempest'); // true

console.log(this.ajaxSettings.type === 'PUT'); // true

console.log(this.ajaxSettings.contentType ===

'application/x-www-form-urlencoded'); // true

var data = JSON.parse(this.ajaxSettings.data.model);

console.log(data.id === '2-the-tempest');

console.log(data.author ==='Tim Shakespeare');

console.log(data.length === 123);

Backbone.sync is called every time Backbone tries to read, save, or delete models. It uses jQuery or Zepto’s $.ajax() implementations to make these RESTful requests, but you can override this as per your needs.

Overriding Backbone.sync

You can override the sync method globally as Backbone.sync, or at a finer-grained level, by adding a sync function to a Backbone collection or to an individual model.

Since all persistence is handled by the Backbone.sync function, we can use an alternative persistence layer by simply overriding Backbone.sync with a function that has the same signature:

Backbone.sync = function(method, model, options) {

};

The following methodMap is used by the standard sync implementation to map the method parameter to an HTTP operation and illustrates the type of action required by each method argument:

var methodMap = {

'create': 'POST',

'update': 'PUT',

'patch': 'PATCH',

'delete': 'DELETE',

'read': 'GET'

};

If we wanted to replace the standard sync implementation with one that simply logged the calls to sync, we could do this:

var id_counter = 1;

Backbone.sync = function(method, model) {

console.log("I've been passed " + method + " with " + JSON.stringify(model));

if(method === 'create'){ model.set('id', id_counter++); }

};

Note that we assign a unique id to any created models.

The Backbone.sync method is intended to be overridden to support other persistence backends. The built-in method is tailored to a certain breed of RESTful JSON APIs—Backbone was originally extracted from a Ruby on Rails application, which uses HTTP methods like PUT in the same way.

The sync method is called with three parameters:

method

One of create, update, patch, delete, or read

model

The Backbone model object

options

May include success and error methods

We can implement a new sync method using the following pattern:

Backbone.sync = function(method, model, options) {

function success(result) {

// Handle successful results from MyAPI

if (options.success) {

options.success(result);

}

}

function error(result) {

// Handle error results from MyAPI

if (options.error) {

options.error(result);

}

}

options || (options = {});

switch (method) {

case 'create':

return MyAPI.create(model, success, error);

case 'update':

return MyAPI.update(model, success, error);

case 'patch':

return MyAPI.patch(model, success, error);

case 'delete':

return MyAPI.destroy(model, success, error);

case 'read':

if (model.attributes[model.idAttribute]) {

return MyAPI.find(model, success, error);

} else {

return MyAPI.findAll(model, success, error);

}

}

};

This pattern delegates API calls to a new object (MyAPI), which could be a Backbone-style class that supports events. This can be safely tested separately, and potentially used with libraries other than Backbone.

There are quite a few sync implementations out there. The following examples are all available on GitHub:

Backbone localStorage

Persists to the browser’s localStorage

Backbone offline

Supports working offline

Backbone Redis

Uses Redis key-value store

backbone-parse

Integrates Backbone with Parse.com

backbone-websql

Stores data to WebSQL

Backbone Caching Sync

Uses localStorage as cache for other sync implementations

Dependencies

The official Backbone.js documentation states:

Backbone’s only hard dependency is either Underscore.js ( >= 1.4.3) or Lo-Dash. For RESTful persistence, history support via Backbone.Router and DOM manipulation with Backbone.View, include json2.js, and either jQuery ( >= 1.7.0) or Zepto.

What this translates to is that if you require working with anything beyond models, you will need to include a DOM manipulation library such as jQuery or Zepto. Underscore is primarily used for its utility methods (which Backbone relies upon heavily) and json2.js for legacy browser JSON support if Backbone.sync is used.

Summary

In this chapter I have introduced you to the components you will be using to build applications with Backbone: models, views, collections, and routers. We’ve explored the Events mixin that Backbone uses to enhance all components with publish-subscribe capabilities and seen how it can be used with arbitrary objects. Finally, we saw how Backbone leverages the Underscore.js and jQuery/Zepto APIs to add rich manipulation and persistence features to Backbone collections and models.

Backbone has many operations and options beyond those we have covered here and is always evolving, so be sure to visit the official documentation for more details and the latest features. In the next chapter, you will start to get your hands dirty as we walk through the implementation of your first Backbone application.