Chapter 3

Backbone.js

Backbone gives your app structure. It provides a sensible framework that stores your app's data, relaying that data to the user as onscreen content. It can also manage syncing that data with the database or local storage. In this chapter, you find out how to store your app's data using Backbone models and collections. You then learn how to tie that data to the content that gets displayed to the user. Any changes in the content will be automatically updated on the screen. Additionally, you discover how to sync Backbone data with a database server, allowing you to save and fetch the data and create a state that is persistent across different sessions.

This chapter also covers how to tie different URLs to different states of your app, enabling you to build navigation and bookmarking support for your app. Finally, you find out how to set up custom handlers for any changes in your data, and also how to manipulate and sort collections.

Getting Started with Backbone

Although Backbone isn't necessarily easy to learn, it's certainly worth the effort, and if you already have experience with a back-end Model-view-controller (MVC), getting started with Backbone will be a lot easier. That's because the concepts of Backbone are harder to grasp than the actual implementation and usage.

What Is Backbone?

Backbone is a JavaScript MVC framework, which brings all the concepts of an MVC pattern commonly found on server side frameworks, to the front end. Basically, it allows you to decouple your app's data from the view and user interface that is displayed to the viewer.

Backbone provides methods for saving your data in models. You can then sync these models to the view. That means that whenever the data changes, those changes are automatically reflected in the content on the screen. Additionally, these models can be synced with the server using Ajax requests. Again, any changes can be automatically synced in both directions, ensuring that the data in your app stays persistent across different pages and sessions.

Some people have argued that Backbone isn't truly an MVC, that it's more of an MVP (Model-View-Presenter). Although the argument isn't completely semantic, it's a bit inane, similar to the classic nerd argument of whether you prefer Star Wars or Star Trek. The point is that the way you use Backbone is similar to the classic patterns in an MVC. It is designed to separate concerns and decouple the presentational layer from the data.

Why Should You Use Backbone?

Everything that Backbone does, you could build in yourself. In fact you've probably already built a lot of it. But Backbone takes all of those disparate scripts, and unifies them in a single framework that is written well and thoroughly tested. In short, Backbone makes handling data a piece of cake. It avoids the spaghetti-code that is typically written when associating JavaScript data with onscreen content, and syncing that data via Ajax.

When you use Backbone, the data in your app remains completely isolated from the content that is displayed to the user. It also maintains a separation between the data and the processes for syncing it with the server.

Backbone streamlines the development process, allowing you to develop the different components of your app independently with interchangeable parts.

Backbone Basics

Backbone has four basic components:

• Models represent individual objects of data.

• Collections are groups of those models.

• Views represent the parts of the user interface and can display the data in models and collections to the user.

• Routers handle the relationship of the views with the URL in the browser, so when the user navigates to a certain URL, it displays a specific state of your application.

The relationship between these components is illustrated in Figure 3-1.

9781118524404-fg0301.eps

Figure 3-1 In Backbone, models can be grouped into collections. These can then be used to define views, which often have parent and subviews. All of this is driven by routers (optionally).

But Backbone is so much more than these basic building blocks. It has change events that update the view whenever a model changes. It has validation to ensure that all the data is properly formed. And it can be set up to automatically sync the data with an external server or local storage to ensure persistent models.

When to Use Backbone

Once you start using Backbone, you'll wonder how you built certain projects without it. However, you'll also find out that Backbone isn't for everything. In fact, for a lot of projects, Backbone is overkill.

Backbone is best suited for apps that need to handle a lot of data, especially if that data is intimately tied to the content on the screen. It's also ideal for apps that have a lot of data going back and forth from the server. However, it's not really a good idea to use Backbone for less interactive sites. Even if the site uses a lot of JavaScript, you have to ask yourself whether the site handles a lot of data. You can always build in JavaScript using stand-alone libraries and plugins, and even use JavaScript template engines without touching any Backbone.

This isn't a filesize issue, since Backbone is pretty lightweight. Rather it's a development issue—programming with Backbone requires a lot of extra set up. On data-driven sites and apps, that extra set up will actually save you time in the long run, but on more static sites, it's just not worth the extra time and effort.

Setting Up Backbone

To use Backbone, first download the core from http://backbonejs.org. You'll also need to download Backbone's only dependency, Underscore.js, from http://underscorejs.org/.

Then include both scripts on the page, with Underscore first:

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

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

Even when combined, these files are extremely lightweight, adding up to less than 10K when gzipped.

Backbone doesn't need to work with jQuery or any other library, but it does play nicely with any jQuery-type library. Most of the examples in this chapter use jQuery, so make sure to include it before the Backbone script above. Now, you're ready to dig into Backbone and start building apps with structure.

Even though Backbone doesn't explicitly require jQuery or Zepto for its basic functionality, you'll need one of those libraries if you want to do any DOM manipulation inside of views and for RESTful persistence in models.

Models in Backbone

Models in Backbone are the basic building blocks of your app's data layer. For instance, you might have an object of data to represent a user in your app:

user: {

  username: 'jonraasch',

  displayName: 'Jon Raasch',

  bio: 'Some nerd who writes web books',

  avatar: 'j134jks.png'

}

In Backbone, this data is saved as a model. But Backbone's models do so much more than store this data. They contain smart defaults, change events, validation, and more.

A good Backbone setup starts by defining these models for your app's data schema. This won't necessarily have a one-to-one relationship with your back-end data schema, but it will probably be close. Next, you set up event listeners to track any changes to the model. But most importantly, you also create a set of validation rules that will run against your model's properties before the save action executes. That way, you can create unit tests for the business logic of your app and weed out potential data issues before they arise. Having this clear separation between your models and the views that represent them can make writing tests for your business logic a whole lot easier. You can focus solely on what each specific piece of functionality aims to achieve without worrying about visual elements that may or may not be on the page.

Creating a Model

To create a new model, use Backbone's Model.extend() method:

// create the model

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

// create a new instance of the model

var apple = new Fruit({

  type: 'apple',

  color: 'red',

  condition: 'shiny'

});

Now, you can access any attribute of the model using the get() method:

console.log( apple.get('color') );

You can also set any attribute value using the set() method:

apple.set('condition', 'bruised'),

apple.set({ type: 'banana', color: 'yellow' });

As you can see, you can either set a key-value pair or set a group of attributes using an object.

Creating Computed Attributes

You can also create custom computed attributes, for example:

var Fruit = Backbone.Model.extend({

  description: function() {

    return this.get('color') + ', ' + this.get('condition') + ' ' + this.get('type'),

  }

});

var apple = new Fruit({

  type: 'apple',

  color: 'red',

  condition: 'shiny'

});

console.log( apple.description() );

This example returns the string red, shiny apple. Notice how you're using the get() method with interpolated strings.

Setting Defaults

You can also set up smart defaults for your model. That way, you can create a new item without setting all the attributes:

var Fruit = Backbone.Model.extend({

  defaults: {

    condition: 'perfect'

  }

});

var apple = new Fruit({

  type: 'apple',

  color: 'red'

});

console.log( apple.get('condition') );

Here you set up a default for the condition of the fruit. This snippet outputs the condition as “perfect,” even though you haven't set it.

Defaults can save you a lot of time in cases where an attribute is most often one way or another. But don't get too heavy handed with them; there are still some attributes that you should set every time you create an object. For instance, you don't want to set a default type of fruit here, because each one needs to be different.

Using the Initialize Function

The initialize function is a constructor that gets called when the model instantiates. To use it, you simply assign a function to that object property when passing in the configuration object:

var Fruit = Backbone.Model.extend({

  initialize: function() {

    console.log('Fruit model initialized'),

  }

});

This callback fires whenever you create a new instance of this model.

Using Backbone Events

Backbone's models also contain a number of events, which allow you to track whenever a model is added, removed, modified, and so on.

Binding Events to Your Model

You can bind any of these events using the on() method within the initialize() function. For instance, to trigger an event whenever a new item is added with your model, you write

var Fruit = Backbone.Model.extend({

  initialize: function() {

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

      console.log('Fruit added - ' + this.get('type'));

    });

  }

});

Notice how I use this.get() to access one of the attributes of this model, the context of each event handler is bound the model instance itself.

Tracking Model Changes

Probably the most widely used event in Backbone is the change event because it allows you to bind changes in your model with visual changes in the view. I talk about the view in greater detail later this chapter, so for now I'll keep it simple and ignore actually changing the DOM.

To track any change to your model, you write

var Fruit = Backbone.Model.extend({

  initialize: function() {

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

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

    });

  }

});

This callback fires whenever an attribute changes in your model.

Additionally, you can track changes to an individual attribute of your model using change:[attribute]:

var Fruit = Backbone.Model.extend({

  initialize: function() {

    // track changes in the condition attribute

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

      console.log('The condition of this fruit has changed.

  Might be getting moldy.'),

    });

  }

});

This callback fires only when the condition attribute of a model changes, as in this example:

apple.set('condition', 'moldy'),

When attaching change events for multiple attributes, beware of execution order issues. It's difficult to determine which attribute listener will fire first, so either make sure the callbacks are completely independent or bind a single change event and check for changes on each attribute manually.

Validating Your Model

Probably the most important part of setting up your model is creating validation. That way, you can do a sanity check and prevent malformed data from ever being saved. For example, you can set up validation to make sure a certain property is numeric:

var Fruit = Backbone.Model.extend({

  // validate the model whenever it changes

  validate: function( options ) {

    if ( options.quantity && !_.isNumber( options.quantity ) ) {

      return 'Quantity must be a number';

    }

  }

});

// create a new instance of the model

var apple = new Fruit({

  name: 'apple'

});

// add an error event handler – this fires if it fails validation

apple.on( 'error', function( model, error ) {

  console.log( error );

});

// set a malformed quantity to trigger a validation error

apple.set( 'quantity', 'a bunch' );

Here's what's going on in this snippet:

1. I'm creating a validation function to check for malformed data in the fruit model.

2. If quantity is set, Underscore's isNumber() method checks that it's a number. Underscore is a prerequisite for Backbone, so there's no additional weight for using its methods.

3. An error event fires if the model fails validation.

The result is that if someone tries to set the quantity property to a non-numeric value, it triggers the error handler and passes the validation error message Quantity must be a number. More importantly, this data will never be saved to the model, ensuring that the model's data remains properly formed.

Since model validation is so important, it's a good idea to set up unit tests on your models to check that they always match your app's business logic.

Working with Collections in Backbone

In Backbone, a collection is a group of models. If models are objects, you can think of a collection as an array of these objects. Although collections in Backbone don't do much more than group these models, they're still important to understand. After all, you'll probably end up using a lot of them.

Creating a Collection

This code creates a collection of items using the fruit model:

// create a model

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

// create a collection with this model

var Fruits = Backbone.Collection.extend({

  model: Fruit

});

Here, the collection is based on the model. Now you can add a new item to this collection schema:

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

var Fruits = Backbone.Collection.extend({

  model: Fruit

});

// create a new instance of the collection

var fruitbowl = new Fruits({ type: 'apple', color: 'red' });

// add another model to the collection

fruitbowl.add({ type: 'banana', color: 'yellow' });

Here you can see two ways of adding items to the collection. First, you can add an item when the collection is created; then you can add others using add().

Alternatively, you can add multiple items when you create the collection by passing an array of objects.

Creating Collection Events

As with models, you can attach event handlers to your collections. All the same events are available: add, remove, change, and so on. However, with collections, you'll probably find yourself using a few new patterns: Namely, the add and remove events become much more important because you'll want to track whenever new items are added or removed from the collection array. For example, this script fires events whenever an item is added or removed from a collection:

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

var Fruits = Backbone.Collection.extend({

  model: Fruit,

  initialize: function() {

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

      console.log('New fruit added'),

    });

    

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

      console.log('Fruit removed'),

    });

  }

});

Note that this won't trigger the add event handler callback when instantiating a new collection with objects.

You can also attach these events to the model for the same result.

Understading Backbone Views

In Backbone, views represent the user interface elements of your application that can often represent your data models. Although they're intimately related to the markup on the page, don't think of views as the markup for your app; rather, they are the logic for rendering markup.

Although you could use Backbone without views, the view is where the framework really starts to shine, because you can bind events for any changes that happen in your models and collections to the view, thereby tying any changes in your app's data to visual changes in the DOM. If you haven't tried this out before, you'll be amazed at what it does to your workflow. In my opinion, binding the data to views is the primary reason to use Backbone, because it eliminates the large amount of spaghetti code you'd use otherwise.

Creating a View

Creating a view is similar to creating a model or collection:

var FruitView = Backbone.View.extend({

  el: '#my-element',

  

  render: function() {

    this.$el.html('Markup here'),

    

    return this;

  }

});

var appleView = new FruitView({

  model: apple

});

Several things are going on in this snippet:

1. The el refers to the element that the view is inserted into. In this case, Backbone finds the element in the DOM that matches the CSS selector #my-element. This is one of a few different ways to create view elements.

2. The render function is called to render the view. This is an optional function that is not called automatically. In this case, jQuery's html() is used to render the content.

3. When a new instance of this view is created, an instance of the model that's to be tied to the view is passed in. This approach is optional; it's just a good practice for any view that is bound to a particular model. That way, you can access the model at any point in the view using this.model.

Using Render Functions

Render functions are completely optional. In fact, they're a common convention rather than part of Backbone itself. It's up to you to create them and call them when necessary, for instance whenever a particular model changes.

Calling the Render Function

Most times, you'll want to call the render function when the view initializes; otherwise, your view won't get displayed on the page. For example, you render the following view when it's created:

var MyView = Backbone.View.extend({

  el: '#my-element',

  initialize: function() {

    this.render();

  },

  

  render: function() {

    this.$el.html('Hello World!'),

    

    return this;

  }

});

var myView = new MyView ();

Here, when the view is initialized, it finds the DOM element with the ID my-element, and replaces the inner HTML of the element with ‘Hello World!'.

The render function returns this. That's a pretty common pattern, because it allows the render function to remain chainable like the other parts of the Backbone API.

Often, you will want to bind the render function to changes in a particular model. You'll soon find out how to do that using a callback from a change event.

Rendering a Model

In most cases, Backbone views are tied to a model, which means that you probably need to access attributes of that model in the render function. For example, you may want to render data about a particular user. First, start with the model:

// create the model for the user

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

var user = new User({

  username: 'jonraasch',

  displayName: 'Jon Raasch',

  bio: 'Some nerd'

});

Next, create a view that is tied to this model:

// create the view

var UserView = Backbone.View.extend({

  el: '#user-card',

  

  initialize: function() {

    this.render();

  },

    

  render: function() {

    // create a link to the user's profile as a wrapper

    var $card = $('<a href="/users/' + this.model.get('username') +

      '">'),

        

    // add the user's name

    var $name = $('<h1>' + this.model.get('displayName') +

      '</h1>').appendTo($card);

        

    // add the user's bio

    var $bio = $('<p>' + this.model.get('bio') +

    '</p>').appendTo($card);

    // append this element to the DOM

    this.$el.html ($card);

    return this;

  }

});

// create a new instance of the view, tying it to the user model

var userView = new UserView({

  model: user

});

This code demonstrates a couple of new patterns. For one, jQuery is being used to build various bits of markup for the view. To drive the content of this markup, you leverage values from the model by using this.model.get(). These refer to whichever model is set when you create a new instance of the view. Finally, the element that was created is appended to the DOM.

Best Worst Practices

You may have noticed that the code for this render function is pretty sloppy. It mixes a lot of markup in the JavaScript and is the epitome of the spaghetti code that coders try to avoid. For now, I will mix jQuery and markup to make the examples easier to follow. However, this is actually a bad practice. In the next chapter, I show you how to use templates to separate your markup from your JavaScript, an approach you should use 99 percent of the time. But in the meantime, just bear with me.

Using the View Element in Backbone

Whenever you create a view in Backbone, the view is necessarily tied to an element. That's the point in the DOM where the content will be rendered. Depending on the situation, you can either reference an existing element in the DOM or create a new view element on the fly.

Accessing the View Element

Because the view element is so important in Backbone, you can use a couple of different methods to expose it. First, you can access it along the view object you create:

var myView = new MyView();

// log the DOM reference of the view's element

console.log( myView.el );

As you can see, you can access the view element at any time using el.

Additionally, Backbone provides a jQuery/Zepto reference to the view element. You can access this reference using $el:

var myView = new MyView();

// hide the view element using jQuery

myView.$el.hide();

However, this works only if you're using jQuery, Zepto, or a similar library. Like I said earlier, Backbone plays well with these libraries, but it doesn't necessarily need to be used with them.

$el is an example of how Backbone has been evolving with the community. Prior to version 0.9, it had become a common pattern to define this yourself in the view's initialize function with this.$el = $(this.el).

Referencing an Existing Element

In many cases, it makes sense to reference an element that already exists in your markup. To reference an existing element, simply pass a CSS selector to el when you create your view:

var MyView = Backbone.View.extend({

  el: '#my-element',

    

  render: function() {

    this.$el.html('Markup here'),

        

    return this;

  }

});

Here, Backbone finds the element with the ID my-element and uses it for the view. You can pass any CSS selector you want. However, make sure you pass a CSS selector string as opposed to a DOM reference. For example, you can't pass a jQuery reference such as $(‘#my-element').

When referencing an existing element, make sure that your CSS selector references a unique element, unless you want the view to modify all the elements you reference.

Referencing an existing element is a good idea if you have a fairly static piece of your page that needs to be altered based on the data in Backbone. Say that you have a welcome message in your header (for example, Welcome Kate) that needs to be changed if the user modifies her display name. Take a look at some code, starting with the markup:

<header>

  <div class="welcome-message">Welcome</div>

</header>

Next, create a model for the user that contains her display name:

// create a model for the user

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

var user = new User({

  displayName: 'Kate'

});

Once you have your model, you can set up the view:

// create the view for the welcome message

var WelcomeMessageView = Backbone.View.extend({

  // tie it to the element that exists on the page

  el: 'header .welcome-message',

    

  initialize: function() {

    // bind any changes in this view's model to its render function – in

    // this case you only need to track changes in the displayName

    // attribute

    this.model.on( 'change:displayName', this.render, this );

        

    // also call the render function when the view initializes

    this.render();

  },

    

  // the render function displays data the model data on the page

  render: function() {

    var displayName = this.model.get('displayName'),

    

    this.$el.html('Welcome ' + displayName);

        

    return this;

  }

});

// create a new instance of the welcome message view

var welcomeMessageView = new WelcomeMessageView({

  model: user

});

There's a lot going on here, so allow me to walk you through it:

1. The view is created and associated with the <div class=”welcome-message”></div> in the markup.

2. The initialize function is created and calls the render function whenever there is a change in the display name of your user. It also calls the render function when a new instance is created for the view so that it renders a solid base state.

3. In the render function, the value of the user model's displayName property is used to display the welcome message.

4. Finally, a new instance of the view is created. It passes in the user model created earlier.

Now, any changes in the model are bound to visual changes in the browser. To test it, try changing the model in your JavaScript console:

user.set('displayName', 'Katherine'),

You should see the change occur in the welcome message on the page.

Creating a New View Element

You've already learned how to reference an existing DOM element for your view, but sometimes you'll want to create a new element for a view in Backbone. To do so, simply set the tagName when you create your view:

var MyView = Backbone.View.extend({

  tagName: 'li'

});

var myView = new MyView;

You can also set a class name and/or ID for your element:

var MyView = Backbone.View.extend({

  tagName: 'li',

  className: 'container', // you can use multiple class names here,

    // e.g. 'container list-item'

  id: 'my-view-wrapper'

});

var myView = new MyView;

This creates a <li class=”container” id=”my-view-wrapper”></li>.

Although these settings are all useful for creating exactly the right markup element, they are all in fact optional. If you don't set anything (and don't set an el), Backbone simply uses a <div></div> without any class or ID.

If you're creating a view element on the fly, be sure not to set the el.

This approach makes the most sense when you're injecting new content into the DOM. For example, say that you want to create a list of elements for each item in a collection. Although the list may live in an existing DOM element, you'll want to create a new view element for each item. I walk through a practical example of this approach in the next section.

Using Nested Views in Backbone

When you build your app in Backbone, you'll use a lot of nested views. Although they can get a bit hairy, nested views are a necessary evil. Fortunately, a number of best practices can help you keep nested views more organized.

To make this happen, you're going to have to roll up your sleeves. Take a look at some code, starting with the markup:

<ul id="band-wrapper"></ul>

Because you're populating this list with Backbone, the markup is straightforward. Next, you create the collection:

var Band = {};

// create the model

Band.Member = Backbone.Model.extend({});

// create the collection

Band.Members = Backbone.Collection.extend({

  model: Band.Member

});

// populate the collection

var band = new Band.Members([

  { name: 'John' },

  { name: 'Paul' },

  { name: 'George' },

  { name: 'Ringo' }

]);

As you can see here, you're creating a collection of four band members.

Creating a View for Each List Item

Now, you create a view for each band member:

Band.Member.View = Backbone.View.extend({

  tagName: 'li',

    

  render: function() {

    // add the name to the list item

    this.$el.text(this.model.get('name'));

        

    return this;

  }

});

The code here is straightforward: a <li> is created for each band member, and then it is populated with the member's name. But how are you going to get these elements on the page and render the complete list of band members? Well, to do that, you need a parent view.

Creating a Parent View for the List

The Band.Member.View already builds markup for each band member on the fly. Now, it's time to reference the element on the page and fill it with the individual band member views. To accomplish this, you create a second view for the overall list, which acts as your parent view:

// create a view for the band

var Band.Members.View = Backbone.View.extend({

  el: '#band-wrapper',

    

  initialize: function() {

    this.render();

  },

    

  render: function() {

    // loop through all of the items in the collection, creating a

    // view for each

    this.collection.each(function(bandMember) {

      var bandMemberView = new Band.Member.View({

        model: bandMember

      });

    });

        

    return this;

  }

});

// create a new instance of the band view

var bandView = new Band.Members.View({

  collection: band

});

Again, a lot is going on here:

1. A new view is created for the list that is linked to the <ul> markup from the beginning of this example.

2. When this view is initialized, it is rendered. That render function loops through the collection of band members, creating a new view for each using the band member view set up earlier.

3. A new instance of the overall list view is created. It passes in the collection of band members. You can pass in collection similarly to the way model was passed in previous examples.

Linking the Parent and Child Views

But you're not done yet. You still have to actually render the individual items in the list. To do that, you link the two views together. First, you need to make some changes to the parent view's render function:

// create a view for the band

var Band.Members.View = Backbone.View.extend({

  el: '#band-wrapper',

    

  initialize: function() {

    this.render();

  },

    

  render: function() {

    // empty out the view element

    this.$el.empty();

        

    // cache this before entering the loop

    var thisView = this;

        

    // loop through all of the items in the collection, creating a

    // view for each

    this.collection.each(function(bandMember) {

      var bandMemberView = new Band.Member.View({

        model: bandMember

      });

            

      // save a reference to this view within the child view

      bandMemberView.parentView = thisView;

            

      // render it

      bandMemberView.render();

    });

        

    return this;

  }

});

The first change is to empty out the view element, which is necessary because you'll be filling it up with the new list items in the child view. But don't worry about that now. You come back to it later. Next, the value of this is cached as thisView. Later, you'll need that value in the collection loop, which will overwrite the this context.

Now, for the important part: Within the collection loop, you're defining a reference to the parent view that you'll be able to access within the child. Because it allows you to make sense of how the views are related to each other, this is a crucial practice when dealing with nested views.

Next, you call the child view's render function. Previous examples called the render function from the view's initialize function. But that doesn't work here because you need the parentView reference, which isn't defined when the child view initializes.

Last but not least, you have to modify the child view's render function to connect with the parent view:

// create a view for each band member

var Band.Member.View = Backbone.View.extend({

  tagName: 'li',

    

  render: function() {

    // add the name to the list item

    this.$el.text(this.model.get('name'));

        

    // append the new list item to the list in the parent view

    this.parentView.$el.append( this.$el );

        

    return this;

  }

});

Here, the only change is that you are referencing this.parentView.$el to grab the view element from the parent view, which you're then using to append the list item. With the parent and child views connected, the band member list renders as shown in Figure 3-2.

9781118524404-fg0302.tif

Figure 3-2 The band list is rendering properly now.

Tracking Changes in the Collection

Finally, you want to track any changes that are made to the collection. To do so, you can set up listeners in the parent view's initialize function:

// create a view for the band

var Band.Members.View = Backbone.View.extend({

  el: '#band-wrapper',

    

  initialize: function() {

    // share the "this" context with the render function

    _.bindAll( this, 'render' );

        

    // add various events for the collection

    this.collection.on('change', this.render);

    this.collection.on('add', this.render);

    this.collection.on('remove', this.render);

        

    // render the initial state

    this.render();

  },

    

  render: function() {

    ...

  }

});

Fortunately, this part is a little easier to follow:

1. The use of _.bindAll() ensures that the this context is shared with the render function.

2. A number of different events are set up for the collection. First, there's a change event in case any collection item changes its values. Then add and remove events track the overall number of items in the collection.

To test that your event listeners are working, add a new band member in your JavaScript console:

band.add({name: 'Yoko'});

As you can see in Figure 3-3, the view updates in the browser.

9781118524404-fg0303.tif

Figure 3-3 Any changes to the collection are being rendered on the page.

Here, the entire list is re-rendered even if one name changes. It's a lot easier to code this way, but the performance is slightly worse than if you only re-rendered an individual list item. However, chances are that the performance loss will be completely unnoticeable, and not worth the extra effort. Remember to avoid premature optimization until you see a performance issue.

Putting It All Together

Just to wrap up, here's the entire example of nested views:

var Band = {};

// create the model

Band.Member = Backbone.Model.extend({});

// create the collection

Band.Members = Backbone.Collection.extend({

  model: Band.Member

});

// populate the collection

var band = new Band.Members([

  { name: 'John' },

  { name: 'Paul' },

  { name: 'George' },

  { name: 'Ringo' }

]);

// create a view for each band member

Band.Member.View = Backbone.View.extend({

  tagName: 'li',

    

  render: function() {

    // add the name to the list item

    this.$el.text(this.model.get('name'));

        

    // append the new list item to the list in the parent view

    this.parentView.$el.append( this.$el );

        

    return this;

  }

});

// create a view for the band

Band.Members.View = Backbone.View.extend({

  el: '#band-wrapper',

    

  initialize: function() {

    // share the "this" context with the render function

    _.bindAll( this, 'render' );

        

    // add various events for the collection

    this.collection.on('change', this.render);

    this.collection.on('add', this.render);

    this.collection.on('remove', this.render);

        

    // render the initial state

    this.render();

  },

    

  render: function() {

    // empty out the view element

    this.$el.empty();

        

    // cache this before entering the loop

    var thisView = this;

        

    // loop through all of the items in the collection, creating a

    // view for each

    this.collection.each(function(bandMember) {

      var bandMemberView = new Band.Member.View({

        model: bandMember

      });

            

      // save a reference to this view within the child view

      bandMemberView.parentView = thisView;

            

      // render it

      bandMemberView.render();

    });

        

    return this;

  }

});

// create a new instance of the band view

var bandView = new Band.Members.View({

  collection: band

});

Just to recap what's going on here:

1. The model and collection of band members is created.

2. Then a view for each individual band member is created. This creates a <li> on the fly and inserts it into the view element for the parent view.

3. A parent view for the overall list of band members is created and tied to the <ul> in the markup.

4. The initialize function for the parent view renders the view and also binds the render function to various changes in the collection.

5. The render function for the parent view empties out the view element; then loops through each item in the collection. For each of these items, it creates a new instance of the child view and passes a reference to itself so the child view can access the parent.

6. The collection loop finally renders the band member view, which appends each <li> to the page, thereby rendering the entire content.

Saving and Fetching Data

Backbone is all about data, and that data won't do much good unless it is able to persist somewhere. Chances are, you'll want to save the data either on the server or in the user's local storage.

Although it doesn't do everything for you, Backbone makes it easy to automatically sync data changes with a persistence layer. Much like views and other parts of Backbone, the framework doesn't do all the work; Backbone just provides structure you can use to make syncing easier.

Syncing a Model on the Server

Saving a model on the server is actually pretty easy in Backbone. In this example, I only cover how to handle this on the front-end; it's up to you to program the backend REST API. Backbone can also be set up to work with local storage. If you'd like to avoid setting up a back-end API, skip ahead to the Using the LocalStorage API with Backbone section; then come back and follow these examples.

In Chapter 6, you find out how to build back ends in Node.js, but for now, just use whatever you're comfortable with: PHP, Rails, and so on.

Saving the Model

First, start with a simple model of user data:

// create the model

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

// create a new user

var user = new User({

  displayName: 'Jon Raasch',

  username: 'jonraasch'

});

Now, to save this model on the server, you only have to make a couple of changes:

// create the model

var User = Backbone.Model.extend({

  // url of the REST API

  url: './path-to-api/'

});

// create a new user

var user = new User({

  displayName: 'Jon Raasch',

  username: 'jonraasch',

  bio: 'Some nerd.'

});

// save it

user.save();

As you can see, you made only two changes to save this model on the server:

1. A URL points to your REST API. This URL is added to the model declaration by passing the option url.

2. It is saved using myModel.save().

When you save this model, an HTTP POST is made to the model's URL with the following JSON as payload data:

{

  "displayName":"Jon Raasch",

  "username":"jonraasch",

  "bio":"Some nerd."

}

Fetching from the Server

If you're saving data on the server, you'll probably also need to fetch it. That way, any data you save on the server can be loaded the next time the user visits the page. To fetch a model from the server, simply set up a sync URL and call myModel.fetch():

// create the model

var User = Backbone.Model.extend({

  // url of the REST API

  url: './path-to-api/'

});

// create a new user

var user = new User;

// fetch the data from the server

user.fetch();

Here, this snippet makes an HTTP GET request to ./path-to-api/ and fills the model with the JSON that is returned.

Fetching from the server is also useful when you want to make sure that changes in the data of your models stay synced with the server. For instance, if multiple users can change the models on the back end, you'll need to set up an interval to check and make sure that no one has changed anything:

// check for changes every 5 seconds

setInterval(function() {

  myModel.fetch();

}, 5000);

This snippet polls the server for changes every five seconds. In Chapter 9, you read more about polling techniques and building real-time apps with Backbone and Node.js.

Providing Success and Error Callbacks

Backbone's sync functions also provide success and error callbacks should you need them. For instance, you can use these callbacks when you save your user model:

// save it

user.save({}, {

  // success callback

  success: function() {

    console.log('User saved successfully'),

  },

  

  // error callback

  error: function() {

    console.log('There was a problem saving the user'),

  }

});

As you can see here, you first pass an empty object to your save function and then an options object with the success and error callbacks.

You can also pull information about the error from the error callback's second argument:

// save it

user.save({}, {

  // success callback

  success: function() {

    console.log('User saved successfully'),

  },

  

  // error callback

  error: function(data, err) {

    // pull the status code and text of the error

    console.log('Error: ' + err.status + ' - ' + err.statusText);

  }

});

Here, you're logging the response code and error message to the console. For example, if the page can't be found, this callback will return Error: 404 — Not Found.

It's important to note that the error callback also fires if the model fails Backbone validation—for example, if you set up a validate() function on your model.

You can also set up success and error callbacks when you fetch. The only difference is that the options object is passed as the first argument in fetch():

// fetch the model from the server

user.fetch({

  success: function() {

    console.log( 'User data fetched from server' );

  },

  

  error: function() {

    console.log( 'Unable to fetch user data' );

  }

});

Request Types

Backbone uses a number of different HTTP request methods to fulfill its sync API: POST, GET, PUT and DELETE. These are important for the REST API because they provide relevant context about the request to the server. In fact, one of the best parts about syncing with Backbone is that it handles these different request types automatically. The different requests are used as follows:

POST: When you save a new model

GET: When you fetch

PUT: When you save changes to a model

DELETE: When you destroy a model

Internally, Backbone actually uses CRUD, which stands for create, read, update, and delete. But when it converts these to jQuery or Zepto's Ajax API, it maps them to POST, GET, PUT, and DELETE, respectively.

Emulating HTTP and JSON

If you're having problems syncing in Backbone, there may be a conflict between your server and the way Backbone posts to it. Fortunately, Backbone provides some easy workarounds that will get it working on just about any server.

First, if your server can't handle PUT and DELETE requests, you'll want to set it to emulate HTTP:

// fix problems with certain REST request types

Backbone.emulateHTTP = true;

Adding this snippet will cue Backbone to use POST for what would normally be update or delete operations. Additionally, if your server can't handle requests that are encoded as JSON, set it to emulate JSON:

// fix problems with JSON encoding

Backbone.emulateJSON = true;

This snippet serializes the JSON and posts it under a single parameter, model, for example:

[model] => {

  "displayName":"Jon Raasch",

  "username":"jonraasch",

  "bio":"Some nerd."

}

In general, it's a good idea to set up your server to handle the types of requests that Backbone creates, for a variety of performance reasons. But if that's too difficult, feel free to use these quick fixes.

I have to emulate JSON when I'm developing locally in the Sites directory of my Mac. That's because Mac's built-in server can't handle requests encoded as application/json by default. Configuring Backbone to use emulateJSON ensures that the data is passed as if from a standard web form, using application/x-www-form-urlencoded.

Using the LocalStorage API with Backbone

By default, Backbone syncs to the server using REST APIs, but you can also set it up to use the localStorage API. It's actually really easy. First, download the local storage adapter here: https://github.com/jeromegn/Backbone.localStorage. Then include this script after the Backbone core.

The script does most of the work for you, overwriting the Backbone.sync API (which you learn about later in this section). The only thing you have to do is configure it to use localStorage on each model or collection you want to save.

For example, to save the user model you built earlier in local storage, you write

// create the model

var User = Backbone.Model.extend({

  // define the local store with a unique name

  localStorage: new Backbone.LocalStorage('user-store')

});

The only difference is that instead of defining the URL of a server-side API for your model, you're defining a localStorage store. Now, you can use all the same methods to sync: save(), fetch(), and so on.

Be sure to define the local storage with a name that's unique across your entire domain, or it will overwrite other locally stored data.

Likewise, you can save collections in local storage:

// create the collection

var Users = Backbone.Collection.extend({

  // define the local store with a unique name

  localStorage: new Backbone.LocalStorage('collection-store')

});

Best of all, the local storage adapter doesn't completely overwrite Backbone.sync, which means that you can still save models and collections on the server. Simply set the url for any model you want to save on the server and localStorage for any model you want to save in local storage.

Although local storage is very useful, keep in mind that some older browsers do not support it. You can find a compatibility table here: http://www.html5rocks.com/en/features/storage.

You can also connect Backbone to WebSockets. Chapter 9 explains how to use WebSockets with Node, JS, and Backbone to create real-time apps.

Saving a Collection on the Server

As you may have guessed, you can also sync collections. However, syncing collections can get a bit more complicated.

Fetching a Collection

First, you pull an entire collection down from the server using myCollection.fetch():

// create the model

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

// create the collection

var Users = Backbone.Collection.extend({

  model: User,

  url: './path-to-api/'

});

// create a new collection

var users = new Users;

// fetch the collection from the API

users.fetch();

This snippet fills the collection with all the objects that are returned from the API. It expects a JSON array of objects, , for example:

[

  { "username":"user1", "displayName":"User 1" },

  { "username":"user2", "displayName":"User 2" },

  { "username":"user3", "displayName":"User 3" }

]

Saving a Collection

Although fetching a collection is pretty straightforward, it's a lot more complicated to save it back to the server, because although you can fetch a collection all at once, you need to save the individual models separately. But that doesn't mean you should be iterating through all the models and saving them at once. Rather, you should create listeners on each of the models to save changes whenever they occur.

This is also a great opportunity to talk about some best practices for how to set up syncing in a practical example. To make things easier, I'm going to use local storage for this example. To follow along, download the local storage adapter covered earlier in the Using the LocalStorage API with Backbone section.

If you'd prefer to work with a server-side API, you can set one up for this example as well. But be sure to switch all the localStorage declarations for url locations of the API.

Setting Up the Collection

First, you set up your model and collection:

// create the model

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

// create the collection

var Users = Backbone.Collection.extend({

  model: User,

  // set up the local storage handler

  localStorage: new Backbone.LocalStorage('users')

});

// create a new instance of the collection

var users = new Users;

Here, you've created a collection of users that sync with local storage.

Fetching the Existing Data

Next, fetch any existing data from local storage by adding a fetch() after you create the collection:

// create the model

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

// create the collection

var Users = Backbone.Collection.extend({

  model: User,

  // set up the local storage handler

  localStorage: new Backbone.LocalStorage('users')

});

// create a new instance of the collection

var users = new Users;

// fetch the collection from local storage

users.fetch();

// log the collection

console.log( users.toJSON() );

After you fetch the collection, you use toJSON() on the collection to log the collection to the console. However, at this point, there's no saved data to fetch, so the collection remains empty.

Adding Models to the Collection

Next, you add a model to the collection. But first create a handler on the model that will save any added models for later:

// create the model

var User = Backbone.Model.extend({

  initialize: function() {

    // add handler to save any added model

    this.on('add', this.addHandler);

  },

    

  addHandler: function() {

    // save the model when it's created

    this.save();

  }

});

// create the collection

var Users = Backbone.Collection.extend({

  model: User,

  // set up the local storage handler

  localStorage: new Backbone.LocalStorage('users')

});

// create a new instance of the collection

var users = new Users;

// fetch the collection from local storage

users.fetch();

// log the collection

console.log( users.toJSON() );

As you can see here, you use this.on() to track any added model events. This handler saves any model that's added.

Now, you can add a couple of models to make sure the script is working. In your JavaScript console write

users.add({username: 'user1'});

users.add({username: 'user2'});

Reload the page, and you should see your script console logging out the two new users, as shown in Figure 3-4.

9781118524404-fg0304.tif

Figure 3-4 The two users are added to the collection.

One thing you may notice is that an ID has been added to each user with a pretty long string of numbers. That's because Backbone needs a unique identifier for each item in a collection when it's saved in local storage. Don't worry; these are added automatically by the local storage adapter. You can also add them manually if you prefer; simply define an id attribute for your model.

Tracking Model Changes

Now that you're tracking added models, you need to also track any changes. To do so, attach another this.on() listener:

// create the model

var User = Backbone.Model.extend({

  initialize: function() {

    // add handlers to save any changes to the model

    this.on('add', this.addHandler);

    this.on('change', this.changeHandler);

  },

    

  addHandler: function() {

    // save the model when it's created

    this.save();

  },

  

  changeHandler: function() {

    // only save what has changed

    this.save(this.changed);

  }

});

Here the changeHandler fires whenever a model has changed. But rather than save the entire model, it saves only the pieces that have changed, using Backbone's built-in model.changed value. That value is an object containing only those attributes that have changed.

It won't make a huge difference when dealing with local storage, but it's still an important practice to understand. When syncing to the server, this practice can make a big performance difference—it reduces the size of the request the server has to handle and also shrinks the HTTP request that the user has to upload.

Now, you can test that the changeHandler is working by changing some values in your JavaScript console:

users.first().set('username', 'newUserName'),

This line pulls the first user off the collection and changes the username. When you refresh the new name is saved to local storage.

Tracking Deleted Models

You also need to set up tracking for whenever a model is deleted. For this, you need a new sync method, destroy()for example:

myModel.destroy();

As you may have guessed, this method deletes the model from the persistence layer. Now build this into another listener, to track remove events:

// create the model

var User = Backbone.Model.extend({

  initialize: function() {

    // add handlers to save any changes to the model

    this.on('add', this.addHandler);

    this.on('change', this.changeHandler);

    this.on('remove', this.removeHandler);

  },

  

  addHandler: function() {

    // save the model when it's created

    this.save();

  },

  

  changeHandler: function() {

    // only save what has changed

    this.save(this.changed);

  },

  

  removeHandler: function() {

    // destroy the model from the server

    this.destroy();

  }

});

Now, you can test it in your JavaScript console:

users.remove( users.first() );

This line removes the first item from the collection. If you refresh the page, you'll see that the change has also been saved to local storage.

Putting It All Together

Now the collection syncing script is complete. You piece the code together as follows:

// create the model

var User = Backbone.Model.extend({

  initialize: function() {

    // add handlers to save any changes to the model

    this.on('add', this.addHandler);

    this.on('change', this.changeHandler);

    this.on('remove', this.removeHandler);

  },

  

  addHandler: function() {

    // save the model when it's created

    this.save();

  },

  

  changeHandler: function() {

    // only save what has changed

    this.save(this.changed);

  },

  

  removeHandler: function() {

    // destroy the model from the server

    this.destroy();

  }

});

// create the collection

var Users = Backbone.Collection.extend({

  model: User,

  // set up the local storage handler

  localStorage: new Backbone.LocalStorage('users')

});

// create a new instance of the collection

var users = new Users;

// fetch the collection from local storage

users.fetch();

// log the collection

console.log( users.toJSON() );

To recap what's going on here:

1. A model and collection are created and that collection is tied to local storage.

2. When the collection loads, the data from local storage is fetched so that saved data gets reflected in the collection anytime the user revisits the page.

3. In the model, you set up different event handlers to save()added or modified models, as well as destroy()removed ones.

You can make this script more concise by removing the add and delete handlers, and simply passing this.save and this.delete to the on() event binder. I avoided that for consistency reasons, but feel free to make the change yourself.

Saving Collections in Bulk

Alternatively, you can build in a handler to avoid saving each model separately and, instead, save the entire collection as a single request.

Building a Custom Save Function

For instance, you can build your own save() function for the collection:

var Users = Backbone.Collection.extend({

  model: User,

  url: './path-to-api/',

  

  initialize: function() {

    _.bind( this.save, this );

  },

  

  // create a custom save function

  save: function() {

    // save this to the server using jQuery's AJAX API

    $.ajax({

      type: 'post',

      // pull in the URL from the collection

      url: this.url,

      // convert the collection data to JSON

      data: this.toJSON()

    });

  }

});

Here, you've built a custom save() function for your collection that does the following:

1. Calls jQuery's Ajax API to post the data.

2. Uses the API URL from the collection

3. Converts the collection data to JSON and sends it to the server as part of the payload request.

Now, you can call this function whenever you want by using users.save().

Bulk saving collections alongside the local storage adapter is a little more complicated because of how it works internally. But it's not a major issue since local storage doesn't use requests.

Drawbacks of Bulk Posting

Although this approach may seem easier, it's not necessarily the best one, especially when dealing with a server-side API. That's because you're posting the entire collection back to the server when only a small part of it may have changed. For instance, if you've changed only one attribute of one model, it doesn't really make sense to post the entire collection. Not only will the server have to process a much larger request but also the JSON will be larger, increasing the size of the HTTP request.

In general, it's much better for performance to post individual changes to the API, unless a whole lot has changed at once (in which case, you can avoid sending a ton of different requests and consolidate them).

Using Backbone.sync

If you are using jQuery or Zepto, you probably won't have to touch Backbone.sync. That's because Backbone automatically uses the $.ajax() API from these libraries to handle syncing. However, if you're using another library or your own vanilla JS, you'll have to overwrite Backbone.sync to utilize your own Ajax handler.

When creating your own Backbone.sync implementation, you need to create separate handlers for each request type:

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

  switch(method) {

    case 'create':

      // what to do for create requests

    break;

    

    case 'read':

      // what to do for read requests

    break;

    

    case 'update':

      // what to do for update requests

    break;

    

    case 'delete':

      // what to do for delete requests

    break;

  }

};

To be honest, I've never actually modified Backbone.sync. Chances are you won't have to either, but if you do, it will get a bit complicated because you have to handle all four request types and also all of the various options for syncing.

Working with Routers

Routers allow Backbone to bind a particular view state to a given URL. That means when a user requests a URL, it displays a related set of content in the view.

Routers are very important when you're building a single-page app (SPA). For example, have you ever used an app and then tried to use the back button or bookmark a page, only to find that it brings you back to the start page of the app? That's a perfect example of a poor user experience. Fortunately, routers make back button and bookmarking support a piece of cake.

If you've been building SPAs without Backbone, you've probably already used something to provide back button support, such as Ben Alman's excellent BBQ plugin for jQuery (http://benalman.com/projects/jquery-bbq-plugin/).

How Routes Work

By default, Backbone routers set up routes that work with hashes. For example, if the path to your app is www.mydomain.com/my-app, some routes may look like this:

www.mydomain.com/my-app#step1

www.mydomain.com/my-app#step2

Now, if the user navigates from the app's landing page to #step1 and then to #step2, these links work with his back button. When the user clicks the back button, he will first go to #step1 and then back to the landing page of your app.

But the paths alone don't do anything; you have to set up the app to respond to each hash URL. For instance, JavaScript should display the step 1 content whenever the URL points to #step1 and the step 2 content when it points to #step2. That way, new content will be displayed when the user clicks the back button, just as though routed to a new static URL. Likewise, the user can bookmark any of these hash links or share one of the links with a friend, and the freshly loaded page will still show the correct content.

Later in this chapter, you find out how to create cleaner URLs with pushState.

Setting Up Routers

Assuming you built your app using the object-oriented, decoupled patterns I've been stressing, setting up routers should be pretty easy. The routers themselves are easy to implement. The only problem you might encounter is an app that isn't set up to easily display different routed pages.

Creating Routes

The basic router setup is as follows: Define a couple of routes; then define what your app does on those routes:

var Workspace = Backbone.Router.extend({

  routes: {

    'settings': 'settings', // #settings

    'help':     'help'      // #help

  },

  

  settings: function() {

    // whatever you need to init the settings page

    console.log('Settings init'),

  },

  

  help: function() {

    // whatever you need to init the help page

    console.log('Help init'),

  }

});

Here, set up routes are using an object of key-value pairs:

• The keys are the URL hash for the route—for example, “settings” is #settings.

• The values reference a function to be called when the browser encounters this hash. For example, if the browser goes to #help, it will call the help() function.

Setting Up the History API

Simply setting up your routes isn't enough to establish routing or implement back button support in your app. You still have to invoke the history API. To start up routing, add the following code:

Backbone.history.start();

Now, you see the appropriate message in your console if you change the URL to www.my-app.com/my-app-url#settings. Although this works just fine in most browsers, you may experience some issues in IE. To get around these compatibility problems, be sure to start the history API after the page loads. For instance, if you're using jQuery you would write

$(function() {

  Backbone.history.start();

});

That approach solves any IE issues.

If the app has already rendered the current page and you don't want the route to fire again, you can pass silent: true, e.g. Backbone.history.start({silent: true}).

Navigating

Now, you can trigger different routes just by passing hash URLs to the browser—for example:

<a href="#settings">Settings</a>

Or you could even invoke these with JavaScript:

window.location.hash = 'settings';

Although both of these techniques will successfully route to the different pages of your app, later in the PushState Versus Hashchange section, you learn about a more robust approach that will work with hashes as well as the pushState API. The better approach is to trigger different routes using Backbone.history.navigate():

Backbone.history.navigate('settings', {trigger: true});

Here, Backbone changes the browser's URL hash to #settings. The trigger option indicates that the API should also call the route function for that URL.

In rare cases, you may want to change the route but not register it in the browser's history (for example, not include it in the back button history). That way, the user can bookmark the page or share a link with a friend without having to pass through that page every time she clicks the back button. This approach makes sense if you're doing a lot of meaningless routing but still want to allow linking. To prevent the new route from registering as part of the browser history, pass replace: true:

Backbone.history.navigate('settings', {trigger: true, replace: true});

Setting Up Dynamic Routes

Backbone's routers are pretty comprehensive—you can even use them to set up dynamic routes. For instance, if your app has a search page, you could set up the following:

var Workspace = Backbone.Router.extend({

  routes: {

    'search/:query': 'search', // #search/monkeys

  },

  

  search: function(query) {

    ...

  }

});

This dynamic route passes in anything after the slash as the first argument of the search() function, which means that you can set up a link #search/whatever-they-search-for and pass the search query to the route handler. You can also set up multiple dynamic parameters:

var Workspace = Backbone.Router.extend({

  routes: {

    'search/:query':        'search', // #search/monkeys

    'search/:query/p:page': 'search'  // #search/monkeys/p7

  },

  

  search: function(query, page) {

    ...

  }

});

Here, you've set up pagination for your search page. Now, if the user is on #search/monkeys/p7, she's routed properly. Additionally, if she's on the first page of the search, the router still works without the page parameter. Furthermore, note the interplay between static and dynamic parameters: the static p coexists alongside the dynamic :page.

PushState Versus Hashchange

So far you've used Backbone's routers with the default URL hash implementation. A route using a hash might look like this:

www.my-app.com/path-to-app#route

However, these paths can start to look ugly, especially when dealing with dynamic parameters, such as this:

www.my-app.com/path-to-app#search/monkeys/p7

Fortunately, the HTML5 spec provides pushState, which allows you to route to normal-looking URLs without changing the page in the browser. For instance, those URLs would be changed to the following:

www.my-app.com/path-to-app/route

www.my-app.com/path-to-app/search/monkeys/p7

As you can see, the paths look a lot cleaner with pushState.

Using PushState

Implementing pushState in Backbone is a piece of cake. Simply define it as an option when you start the history API:

Backbone.history.start({pushState: true});

However, if your app doesn't live at the root of your domain, you will experience some problems here because pushState URLs look like normal URLs, which means there's no way for Backbone to discern what is part of pushState and what is part of the path to your app. To get around this, pass the root option:

Backbone.history.start({pushState: true, root: '/path/to/my-app'),

Enabling Backward Compatibility with Modernizr

Since pushState is part of the newer HTML5 spec, you may be wondering what happens in older browsers. Although Backbone won't handle fallbacks automatically, you can set one up easily using Modernizr. You just include Modernizr (which you can find at http://modernizr.com) and then pass the pushState option as follows:

Backbone.history.start({ pushState: Modernizr.history });

Now, supported browsers use pushState, and unsupported browsers revert to the standard hash implementation.

Best Practices for Using pushState

Besides making URLs look prettier, pushState has some important implications for your app's architecture. If you use pushState correctly, it will make your app much more robust across different environments and situations. However, if you use it incorrectly, it can make your app much worse.

The issue primarily comes up when JavaScript isn't working. That's not only a problem for people who have actively disabled JavaScript; it's also an issue when JavaScript breaks and while JavaScript is still loading.

Without JavaScript, hash URLs won't load at all. For example, if a user bookmarks a hashed URL and then revisits that page with JavaScript disabled, that user lands on the app's start page. Additionally, the start page also displays while the JavaScript is loading and then jumps to the correct content once it initializes. pushState gives you a way to fix this issue. Because the pushState paths are just the same as normal URLs, you can handle the page change on the server as well as in the JavaScript.

Thus, when the user revisits a pushState URL, you can generate the appropriate content statically via the back-end-generated markup. That way, the page will still work if the user has disabled JavaScript or the JavaScript is taking a while to load. The user can even navigate through the pages of your app without any JavaScript at all.

Even though your app most likely depends on a lot of JavaScript, always provide as much content as you can without JavaScript, for those rare cases where JavaScript isn't working.

However, this golden opportunity is also a double-edged sword. If you don't provide any content from the server, the app might be completely broken with pushState. The problem won't crop up when the user stays on the page and uses the back button. But if the user bookmarks the page or shares the link with a friend, the page will load from that static URL.

If your app is set up on www.my-app.com/path-to-app and the pushState URL is www.my-app.com/path-to-app/route, make sure that the back end serves up your app on that URL as well. Otherwise, the page will 404, and the app will be completely broken whether your user has JavaScript working or not.

So, even if you don't want to set up meaningful static content on that URL, at the very least include your basic app so that Backbone can kick in and serve the right content with JavaScript. The easiest way to do so is to rewrite all the pages under your app to route back to the homepage of the app. For example, on an Apache web server, you could do this in your .htaccess file using mod_rewrite:

# enable rewrites

RewriteEngine on

# rewrite all the pages under your app back to the app

RewriteRule ^my-app/(.*)$ /my-app [QSA,L]

This quick-and-dirty fix ensures that your app loads properly for JavaScript users. But I strongly encourage you to consider providing some meaningful static content for all your routes.

More About Events

You already know how to attach events such as add, remove, and change to your models and collections using the on() method. In this section you'll learn more about event binding and unbinding in Backbone.

In some of the Backbone literature, you may have noticed bind() being used in place of on(). That's just an older syntax that was deprecated in favor of jQuery's on() pattern.

Unbinding Events

In addition to binding events with on(), you can just as easily unbind events using the off() method:

Fruit.off('remove'),

This snippet unbinds all remove listeners from the model. But if you've attached a number of different listeners, you can remove an individual listener by referencing it as separate function:

var removeCallback = function() {

  console.log('Fruit removed - ' + this.get('type'));

},

removeCallback2 = function() {

  console.log('Bummer'),

};

var Fruit = Backbone.Model.extend({

  initialize: function() {

    this.on('remove', removeCallback);

    this.on('remove', removeCallback2);

  }

});

...

Fruit.off('remove', removeCallback2);

This script unbinds only the second callback.

Finally, you can unbind all events from an object by passing no argument:

Fruit.off();

Triggering Events Manually

Sometimes, you'll want to trigger an event manually. Sure, you could just fire the callback that is bound to the event, but it's often easier or more accurate to trigger the event itself. For instance, say that you have a couple of different callbacks assigned to different objects but still want to fire whatever callback is assigned. Triggering events is easy; just use the trigger method:

Fruit.trigger('add'),

This will trigger the add event on this model, firing whichever callback has been assigned with on().

Binding “this”

One of the biggest stumbling points when dealing with Backbone is the fact that the this context changes whenever you pass a function as a callback. This issue comes up primarily when dealing with events and the on() API, since that is where you see the most callbacks in Backbone.

You may have noticed that some of the examples use lines of code like the following:

_.bind( this.render, this );

This underscore function will bind the object in the first argument, to the value of ‘this' of each specified function when it's called. Typically, that approach is used so that callback functions retain the context of the calling object when they're invoked, rather than an unintended context. In most cases, you can use this easy catch-all technique, but there may come a time when you need to pass an individual context to a certain function. In those cases, you can pass the context as the third argument manually when you assign the callback:

this.model.on('change', this.render, this);

This example is almost identical to other on() handlers you've already seen: It calls the view's render function whenever the model changes. The only difference is the third argument, which is where you can pass the this context.

In this case, the third argument ensures that this persists in the change handler. But you can also pass anything you want here. For example, when you set up nested views, you set up a parentView value in the child view. Sometimes, that value might be a more relevant value of this, in which case you can pass that value instead:

this.model.on('change', this.render, this.parentView);

Here, this.parentView will become the this context in the render function.

The All Event

You can use a variety of events in Backbone:

add

remove

change

change:[attribute]

destroy

sync

error

all

One particular event type to note is all, which is a catch-all listener you can use to track all these events at once. This can be really useful if you want to set up all your event handlers in a single place.

The type of event is passed as the first argument with this handler, which you can use to set up your own switch:

// create the model

var User = Backbone.Model.extend({

  initialize: function() {

    // set up a catch-all listener

    this.on('all', this.allHandler);

  },

  

  // Backbone passes the type of event as the first argument

  allHandler: function(eventType) {

    // switch based on the event type

    switch(eventType) {

      case 'add':

        // whatever you want to do on add

      break;

      case 'remove':

        // whatever you want to do on remove

      break;

      case 'change':

        // whatever you want to do on change

      break;

    }

  }

});

You may be wondering why you'd use this approach instead of separate handlers for each event type. Indeed, if your handlers are all completely independent, you have no need for this type of approach. But if you're depending on one event firing before another, it's best to set each one up manually in an all event, because it's difficult to determine which event fires first in Backbone. Using this approach, you avoid any race conditions.

Manipulating Collections

So far, you've used collections in a pretty basic way. You've used them to group related models and then loop through those items. However, you can do a lot more with collections, such as pull certain items, filter by different keys, and sort based on custom sort functions.

Pulling Collection Items

Once you've added a number of different items to your collection, you can pull them off in a couple different ways. But, first, create a collection so you have something to work with:

// create the model

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

// create the collection

var Fruits = Backbone.Collection.extend({

  model: Fruit

});

var fruitbowl = new Fruits;

// add items to the collection

fruitbowl.add({ type: 'apple', color: 'red', quantity: 3 });

fruitbowl.add({ type: 'apple', color: 'yellow', quantity: 5 });

fruitbowl.add({ type: 'banana', color: 'yellow', quantity: 1 });

fruitbowl.add({ type: 'orange', color: 'orange', quantity: 3 });

Now, you can dig in and pull individual items off this collection.

Pulling Collection Items by Index

First, you can pull any collection item by passing its index to at():

var thirdFruit = fruitbowl.at(2);

This snippet pulls the third item in the collection (since the indexes start with 0), which in this case is a banana.

There are also special functions for pulling the first and last collection items:

// first item

var firstFruit = fruitbowl.first();

// last item

var lastFruit = fruitbowl.last();

Here, the first() and last() functions pull the red apple and the orange, respectively.

Later in the Sorting a Collection section I show you how to sort your collection. However, even if you haven't sorted your collection, you can still pull items based on the order they are inserted.

Matching Certain Collection Items

You can also pull items from your collection based on their attributes. To do so, pass in filter rules to where():

var apples = fruitbowl.where({ type: 'apple' });

This snippet pulls any items with this type from your collection. This can be a single item or multiple items, depending on what is matched (or no items for that matter).

In this example, where() pulls both of the apples as a separate collection. You can then pull an individual apple off of this collection using the index methods used earlier (or a more specific filter).

Sorting a Collection

You can also sort your collection a couple different ways. Sorting is important when pulling collection items by index and also when looping through the collection items in order.

Using “Sort By” Functions

By default, collections stay in insertion order; the first item you add to the collection is first, the second item is second, and so on. But you can alter their default order by setting a comparator function:

fruitbowl.comparator = function(fruit) {

  // sort by quantity

  return fruit.get('quantity'),

};

Here, the items will be sorted according to the value of their quantity attribute. When applied to the collection you created earlier, this comparator changes the order as follows:

[

  {"type":"banana","color":"yellow","quantity":1},

  {"type":"apple","color":"red","quantity":3},

  {"type":"orange","color":"orange","quantity":3},

  {"type":"apple","color":"yellow","quantity":5}

]

As you can see, the items are sorted by quantity. Where two items have the same quantity (the red apple and the orange), the order defaults to the original insertion order.

Creating Custom Sort Functions

So far, you've created a “sort by” comparator function, meaning that it sorts according to the values of a single metric. That's great when you want to sort something in ascending order based on a numeric value, or sort strings alphabetically. That simple type of comparison is used automatically if your comparator function accepts only one argument. But you can also create a custom sort function with a lot more control by passing two arguments to your comparator, as shown here:

fruitbowl.comparator = function( fruit1, fruit2 ) { … }

However, custom sort functions are a bit more complicated. Whereas sort by comparisons simply return a value to compare, custom sort functions must do the sorting themselves. That means you should return the following:

• Return –1 if the first model should come before the second.

• Return 0 if they are of the same rank.

• Return 1 if the second model should come before the first.

For instance, you can sort the fruit in reverse alphabetical order:

// create a custom comparator to sort in reverse alphabetical order

fruitbowl.comparator = function(fruit1, fruit2) {

  // get the names for each fruit

  var fruitName1 = fruit1.get('type'),

  fruitName2 = fruit2.get('type'),

  

  // compare the strings

  if ( fruitName1 < fruitName2 ) return 1;

  if ( fruitName1 > fruitName2 ) return -1;

  return 0;

};

Here, if the second fruit has a higher alphabetical value, you return 1 (because you want the higher alphabetical values to come first). Conversely, if the first fruit has a higher alphabetical value, you return -1. Finally, you return 0 if they are the same. As you can see, the custom comparator places the fruits in reverse alphabetical order:

[

  {"type":"orange","color":"orange","quantity":3},

  {"type":"banana","color":"yellow","quantity":1},

  {"type":"apple","color":"red","quantity":3},

  {"type":"apple","color":"yellow","quantity":5}

]

I kept this example simple, but if you want to get a bit fancier, you can also use the native JavaScript string comparison localeCompare():

// create a custom comparator to sort in reverse alphabetical order

fruitbowl.comparator = function(fruit1, fruit2) {

  // get the type names for each fruit

  var fruitName1 = fruit1.get('type'),

  fruitName2 = fruit2.get('type'),

  

  // compare the strings with localeCompare

  return fruitName2.localeCompare( fruitName1 );

};

localeCompare() is particularly handy for this type of sorting because it returns the appropriate 1, 0 or –1 value. Normally, you use firstString.localeCompare( secondString ), but they are reversed here for reverse alphabetical order.

Manually Triggering Sorting

Comparator functions keep the collection in order automatically. For example, you can add additional items to the collection:

// add items to the collection

fruitbowl.add({ type: 'apple', color: 'red', quantity: 5 });

fruitbowl.add({ type: 'orange', color: 'orange', quantity: 3 });

// create a comparator

fruitbowl.comparator = function(fruit) {

  // sort by quantity

  return fruit.get('quantity'),

};

// add additional items

fruitbowl.add({ type: 'peach', color: 'pink', quantity: 2 });

fruitbowl.add({ type: 'plum', color: 'purple', quantity: 4 });

Even though some items are added to this collection after the comparator function is defined, the collection is still kept in order. In fact, the collection won't be sorted at all until the additional items are added. That's because adding a comparator doesn't trigger a sort—sorting is only triggered by adding new elements. Thus, in some cases, you will need to resort your collection. For example, if you add a new comparator function without adding any new items or if you change the value of a certain attribute, you will need to resort your collection manually. Fortunately, you can accomplish this easily using sort():

// set the quantity of oranges to zero

// this won't automatically change the sort order

fruitbowl.where({ type: 'orange' }).set( 'quantity', 0 );

// trigger a resorting

fruitbowl.sort();

Here, someone ate all the oranges, so you have to manually resort your collection.

Summary

In this chapter, you found out how to use Backbone to bring structure to your app. You started by learning why you should use Backbone, and when to avoid it. Then you discovered the basics of setting up your data in Backbone models and collections.

Next, you learned how to tie the data in models to visual changes on the screen through Backbone views. You also found out how to sync that data to a persistence layer. Then you learned how to use routers to map certain URLs to different JavaScript–generated pages of your app, building in back button and bookmarking support. Finally, you read about how to use Backbone's event handlers to track a variety of events, and how to manipulate collections.

In the coming chapters, you discover how to use a variety of other JavaScript techniques. However, the book will keep coming back to Backbone, because it's the foundation upon which the rest of the code is built.

Additional Resources

Backbone Documentation: http://backbonejs.org/

Underscore Documentation: http://underscorejs.org/

Best Practices in Backbone:

Backbone Boilerplate: https://github.com/tbranyen/backbone-boilerplate

Backbone Patterns: http://ricostacruz.com/backbone-patterns/

Backbone Books:

Developing Backbone.js Applications (free e-book, highly recommended): http://addyosmani.github.com/backbone-fundamentals/

Backbone.js on Rails: https://learn.thoughtbot.com/products/1-backbone-js-on-rails

Backbone Tutorials:

Getting Started With Backbone.js: http://net.tutsplus.com/tutorials/javascript-ajax/getting-started-with-backbone-js/

Build a Contacts Manager Using Backbone.js: http://net.tutsplus.com/tutorials/javascript-ajax/build-a-contacts-manager-using-backbone-js-part-1/

Anatomy of Backbone.js: http://www.codeschool.com/courses/anatomy-of-backbonejs

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset