CHAPTER 3

image

Backbone Models and Collections

There are two main parts to the model layer of any Backbone.js application: the representation of the data (Backbone.Model) and the grouping of your data into sets (Backbone.Collections). An understanding of how each of these parts works and interacts will give you the ability to create a well-structured data layer for your Backbone applications.

image Note  Throughout this chapter, as you are using the code samples, you can simply load index.html from Chapter 1 and use the Chrome Developer Console to try these snippets. Because you have loaded Backbone, Underscore, and jQuery, all the snippets will execute correctly.

System Setup

Before we go further, you will need to do a few things to ensure your system is working properly. First, all your code should be executed froman Apache web server, rather than simply from the file system. In my examples, I havea folder named backbone in the htdocs of an XAMPP installation. When serving index.html from this server, the URL to visit is http://localhost/backbone/index.html.

The structure of index.html should follow that from the previous chapter. Ensure that you have included the Backbone, Underscore, and jQuery libraries, and put any JavaScript code shown in this chapter between with the script element at the end of the file.

You will also notice that for any back-end API calls I used Node.js. This can be downloaded from http://nodejs.org, and you should ensure that your local installation is working properly before setting up the server.js file that we will create in the “Exchanging Data with the Server” section.

Finally, don’t forget to use the Chrome Developer Tools when you run into problems; it always provides the best chance at narrowing down bugs in your JavaScript code.

Backbone Models

Every application begins with the definition of a model. It’s the data that you need to manipulate and represent in the app. As we described in previous chapters, the model is a layer in which the data resides and will likely contain many objects. As a rule of thumb, you should break your problem domain into individual elements and describe each of these as a model object.

To create a model object in Backbone, you will need to use the Backbone.Model. As you’ll see for all Backbone objects that you create, you’ll use an .extend method to create your own version of the class, as in the following code:

MyModel = Backbone.Model.extend({
       //object properties would go here
});
  

All objects in the library have this Backbone namespace, so it’s always really clear when you’re using Backbone objects.

In real applications, it is likely that you will never create a model in this waybut instead will pass through properties to construct your object correctly. In the previous code snippet, the empty brackets ({}) represent an object literal. This is where all the properties and functions that you want to be available in the model will reside.

image Note  An object literal contains a list of comma-separated definitions in the form of key : value, where value can be a function or another object.

Constructors

When creating new model objects, you may want some behavior to be executed on construction, such assetting up different handlers or setting some initial state in your mode. To achieve this in Backbone, just define an initialize function when creating your model object.

To see this initialize function in action, let’s create a simple Book object where a line is written to the console on object creation.

Book = Backbone.Model.extend({
     initialize: function(){
           console.log('a new book'),
     }
});

This initialize function gets called as soon as we create a new instance of the Model object. In this case, on creation of a new Book object, we’ll see the line “a new book” on the console.

varmyBook = new Book();

Another common requirement for models is to have some default attributes available. You may want to do this so that optional parameters, not passed through on object creation, have some definition. In Backbone, this is done using the defaults object literal.

Book = Backbone.Model.extend({
    initialize: function(){
          console.log('a new book'),
          },
  
      defaults: {
              name: 'Book Title',
              author: 'No One'
              }
      });

Now when the object is created, these default values are provided for each instance.

Model Attributes

With data being the main reason that you have a model, let’s first focus on attributes. This section coversall the attribute-related operations that are available to you when using Backbone.Model.

Getting Attribute Values

The attributes in any model object can be easily retrieved by using the .get function and passing through the name of the attribute that you want to access. Because we’re using the simple default values, the following line would print out “Book Title” after you have initialized the Bookobject.

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

During object creation, it is possible to pass through the values of the attributes in the modelso that each instance can be unique.

varthisBook = new Book({name : 'Beginning Backbone',
                                author: 'James Sugrue'});

Now when you retrieve the values of either attribute, the value passed through on creation will be used in place of the default.

console.log(thisBook.get('name') + ' by ' + thisBook.get('author'));

Now when you retrieve the values of either attribute, the value passed through on creation will be used in place of the default.

You can also use the .attributes property to get a JSON object that represents all of the model data.

console.log(thisBook.attributes); // a JSON representation of all attributes

Changing Attribute Values

Changing attribute values outside of the constructor is done in a similar way, using the structure of a function call in the format .set('<variable name>', <value>). For example, to change the book name, use this:

thisBook.set('name', 'Beginning Backbone.js'),

You can add new attributes to your object in the same manner as shown previously.

thisBook.set('year', 2013);//creates a new attribute called year

Deleting Attributes

You may find that you need to delete attributes you have no use for from your model. In the following example, a new attribute is added and subsequently deleted. The second console.log statement results in undefined being returned as the attribute value.

thisBook.set('year', 2013);
console.log('Book year ' + thisBook.get('year'));
thisBook.unset('year'),
console.log('Book year ' + thisBook.get('year'));

Backbone provides a neater way to check for the presence of an attribute in a model, using the .has function, which returns a Boolean.

//check for the existence of an attribute
varhasYear = thisBook.has('year'), //results in false
varhasName = thisBook.has('name'), //results in true
console.log('Has an attribute called year  : ' + hasYear);
console.log('Has an attribute called name  : ' + hasName);

You can also delete all attributes from your model using the clear function.

//create a new object
varnewBook = new Book({name: 'Another Book', author: 'You'});
newBook.clear();//remove all attributes
console.log('Has an attribute called name  : ' + newBook.has('name'));//results in false

Both the unset and clear functions take an additional options object, in which you can add a Boolean indicating that no change event should be triggered from the model when the operation is complete. The “Model Events” section later in this chapter explains how these events operate.

thisBook.unset('year', {silent: true});

Cloning Models

It’s common that you might want to make a complete copy of your Backbone model, keeping all the same attributes. Rather than needing to worry about the details of how to create a deep copy, you can simply use the .clone() method to create a cloned model instance.

varclonedBook = thisBook.clone();

Attribute Function Reference

Table 3-1 describesthe most useful operations relating to attributes that you can carry out on a Backbone model.

Table 3-1. Attribute Functions for Backbone Models

Operation

Description

.get(<attribute name>)

Returns the value of the attribute with the given name. If no such attribute exists, undefined is returned.

.set(<attribute name>, attribute value>

Sets the value of the given attribute to the value provided in the second parameter. If the attribute doesn’t already exist, a new attribute is created with this value.

.has(<attribute name>)

Checks for the existence of the given attribute in the model object.

unset(<attribute name>)

Removes an attribute from the model, if it exists.

clear()

Removes all attributes from the model object.

.attributes

Returns a JSON representation of all attributes in the model.

.clone()

Creates a new instance of the model with all the same attributes.

Adding Functions to Your Model

So far our model has been all about the attributes, but you can also add your own functions to deal with repetitive tasks. The following example illustrates how to include a printDetails function in place of all the console.log statements used so far:

Book = Backbone.Model.extend({
     initialize: function(){
           console.log('a new book'),
     },
        
  defaults: {
    name: 'Book Title',
    author: 'No One'
       },
        
       printDetails: function(){
              console.log(this.get('name') + ' by ' + this.get('author'));
       }
});

This function is now available to all instances of the Book object.

//use the printDetails function
thisBook.printDetails();

Model Events

Although we’ll be dealing with events later in the book, one specific type of event that is critical for models is the change event, where the values of an attribute, or set of attributes, are altered.

Listening for Changes

With Backbone change handlers, the easiest type of change handler listens for changes across the entire model. Event handlers are added to Backbone objects using the .on() function, which accepts the type of handler as a string andaccepts a reference to a function that will be run when the change happens.

The best time to create this listener is in the model initialize function. By altering the code you have so far, you will see that any time a set is called, this function is invoked.

Book = Backbone.Model.extend({
     initialize: function(){
          this.on("change", function(){
                        console.log('Model Changed'),
                });
     },
        
  defaults: {
    name: 'Book Title',
    author: 'No One'
       },
        
       printDetails: function(){
              console.log(this.get('name') + ' by ' + this.get('author'));
       }
});

You can listen for changes in specific attributes by using the format change:<attribute name> rather than change. The following addition creates another handler that deals only with changes to the name attribute:

initialize: function(){
          this.on("change", function(){
                        console.log('Model Changed'),
                });
                this.on("change:name", function(){
                        console.log('The name attribute has changed'),
                });
     },

We noted earlier that the .set function allowed an optional parameter for silent updates. If this is used, then the change handler won’t be invoked.

//set the variable (expect change handler)
thisBook.set('name','Different Book'), //change handler invoked
thisBook.set(,'name', 'Different Book', {silent:true});//no change handler invoked

Figuring Out What Has Changed

Backbone includes a number of properties thatkeep track of what is changed in your model. If you are using a global change handler, this can be a really useful way to see what’s going on.

You can check whether an individual attribute has been altered using hasChanged('<attribute name'>).

this.on("change", function(){
           console.log('Model Changes Detected:'),
           if(this.hasChanged('name')){
                  console.log('The name has changed'),
           }
           if(this.hasChanged('year')){
                  console.log('The year has changed')
           }
   });

You can get a set of all the attributes that have changed using the .changed property.

Book = Backbone.Model.extend({
     initialize: function(){
          this.on("change", function(){
           console.log('Changed attributes: ' + JSON.stringify(this.changed));
             });
     },

You can also get a set of the previous state of all attributes using the .previousAttributes() function. The following line could also be added to the change handler:

console.log('Previous attributes: ' + JSON.stringify(this.previousAttributes()));

Finally, you can retrieve the previous value of a specific attribute using .previous('<attribute name'>).

if(this.hasChanged('name')){
                             console.log('The name has changed from '  + this.previous('name') + ' to ' + this.get('name'));
                              
}

Attribute Changes Reference

Table 3-2 describesthe most useful operations relatedto attributes changes in your model.

Table 3-2. Functions Related to Attribute Changes in Backbone Models

Operation

Description

.on('change', <function>)

Provides a global change handler that responds to any attribute changing in the model

.on('change:<attribute name>', <function>)

Listens for changes on a particular attribute

.hasChanged(<attribute name>

Returns true if the attribute has changed since the last change event

.previous(<attribute name>)

Returns the previous value of a particular attribute

.changed

Returns a complete set of all the changed attributes in the model

Model Validation

Backbone provides a validation mechanism for model data, meaning that you can have all the logic that determines whether the state of the model is correct or not within the model, rather than in some external JavaScript or form-processing code.

If you provide a validation method, it will be run every time the .save function is invoked and during every set/unset operation when {validate:true} is provided as an optional parameter.

Let’s imagine our Book model insists that a name exists and that the year is after 2000. A validation method for these rules would look as follows:

Book = Backbone.Model.extend({
       initialize: function(){
       },
        
defaults: {
       },
        
       printDetails: function(){
       },

       validate: function(attrs){
               if(attrs.year< 2000){
                      return 'Year must be after 2000';
               }
               if(!attrs.name){
                      return 'A name must be provided';
               }
       }
 });

If you break any of these rules when manipulating the model, the operations will fail to change the attribute values.

//try setting the year to pre 2000
thisBook.set('year', 1999, {validate: true});
console.log('Check year change: ' + thisBook.get('year'));
//try removing the name from the model
thisBook.unset('name', {validate: true});
console.log('Check if name was removed ' + thisBook.get('name'));

When a validation error has been detected, an event is fired. By adding an “invalid” event handler, you can provide feedback on the validation error. As with all event handlers, this should be added to your initialize function.

Book = Backbone.Model.extend({
            initialize: function(){
          this.on("invalid", function(model, error){
                 console.log("**Validation Error : " + error + "**");
          });

Without the validation flag, the validation function will not be executed on set. However, you can check whether the model is in a valid state at any time with the isValid() function.

//check if model is valid
console.log('Is model valid: ' + thisBook.isValid());
//break the validation rules by not using the validate flag
thisBook.set('year', 1998);
//check if the model is valid
console.log('Is model valid: ' + thisBook.isValid());

Exchanging Data with the Server

The final set of functionality available for Backbone models relates to how the data can be stored from, and sent to, a server that provides a REST API. Before we get to the Backbone mechanisms around this, we’ll first set up a simple backend to provide responses to our API calls.

image Note  Keep in mind that Backbone.Collection looks after a lot of the RESTful operations, rather than needing to make calls for every model object. To illustrate the underlying mechanisms, we’ll look at the model operations first.

Node.js Server Back End

As we’re working with JavaScript anyway, I will outline a simple server that uses Node.js, but feel free to replace it with a REST server implementation of your own choice. We won’t be using this extensively, as the focus of this book is on the client side. However, it will be useful to respond to Backbone application requests for illustration purposes.

First you’ll need to install Node.js from http://nodejs.org. Once it’s installed, use the NPM package manager to install the express node package, a minimal web application framework for Node.jsthat makes it really easy to get a simple server witha REST API running.

npm install express

Finally, just copy the following code into server.js. As you can see from the code, it provides some simple endpoints served from http://localhost:8080/books.

/**
* A simple API hosted under localhost:8080/books
*/
var express = require('express'),
var app = express();
varbookId = 100;

functionfindBook(id){
   for(vari =0; i<books.length; i++){
       if(books[i].id === id){
           return books[i];
       }
   }
   return null;

}

functionremoveBook(id){
   varbookIndex = 0;
   for(vari=0; i<books.length; i++){
       if(books[i].id === id){
           bookIndex = i;
       }
   }
   books.splice(bookIndex, 1);
}

app.configure(function () {
   //Parses the JSON object given in the body request
   app.use(express.bodyParser());
});

var books = [
{id: 98, author: 'Stephen King', title: 'The Shining', year: 1977},
{id: 99, author: 'George Orwell', title: 1949}];
/**
* HTTP GET /books
* Should return a list of books
*/
app.get('/books', function (request, response) {
    
   response.header('Access-Control-Allow-Origin', '*'),
   console.log('In GET function '),
   response.json(books);

});
/**
* HTTP GET /books/:id
* id is the unique identifier of the book you want to retrieve
* Should return the task with the specified id, or else 404
*/
app.get('/books/:id', function (request, response) {
  response.header('Access-Control-Allow-Origin', '*'),
  console.log('Getting a  book with id ' + request.params.id);
  var book = findBook(parseInt(request.params.id,10));
  if(book === null){
       response.send(404);
  }
  else{
       response.json(book);
  }
    
});
/**
* HTTP POST /books/
* The body of this request contains the book you are creating.
* Returns 200 on success
*/
app.post('/books/', function (request, response) {
   response.header('Access-Control-Allow-Origin', '*'),

   var book = request.body;
   console.log('Saving book with the following structure ' + JSON.stringify(book));
   book.id = bookId++;
   books.push(book);
   response.send(book);

});
/**
* HTTP PUT /books/
* The id is the unique identifier of the book you wish to update.
* Returns 404 if the book with this id doesn't exist.
*/
app.put('/books/:id', function (request, response) {
   response.header('Access-Control-Allow-Origin', '*'),
   var book = request.body;
   console.log('Updating  Book ' + JSON.stringify(book));
   varcurrentBook = findBook(parseInt(request.params.id,10));
   if(currentBook === null){
       response.send(404);
   }
   else{
       //save the book locally
       currentBook.title = book.title;
       currentBook.year = book.year;
       currentBook.author = book.author;

       response.send(book);
   }
});
/**
* HTTP DELETE /books/
* The id is the unique identifier of the book you wish to delete.
* Returns 404 if the book with this id doesn't exist.
*/
app.delete('/books/:id', function (request, response) {
  console.log('calling delete'),
  response.header('Access-Control-Allow-Origin', '*'),
  var book = findBook(parseInt(request.params.id,10));
  if(book === null){
      console.log('Could not find book'),
     response.send(404);
  }
  else
  {
    console.log('Deleting ' + request.params.id);
    removeBook(parseInt(request.params.id, 10));
    response.send(200);
  }
  response.send(200);
    
});

//additional setup to allow CORS requests
varallowCrossDomain = function(req, response, next) {
   response.header('Access-Control-Allow-Origin', "http://localhost");
   response.header('Access-Control-Allow-Methods', 'OPTIONS, GET,PUT,POST,DELETE'),
   response.header('Access-Control-Allow-Headers', 'Content-Type'),

   if ('OPTIONS' == req.method) {
     response.send(200);
   }
   else {
     next();
   }
};

app.configure(function() {
   app.use(allowCrossDomain);
});

//start up the app on port 8080
app.listen(8080);

To kick off the server, use node server.js on the command line. This server is very simple but will do enough to getthe Backbone persistence calls to execute correctly.

Identifiers

Backbone models have three attributes that deal with uniquely identifying them during data exchange with the server: id, cid, and idattribute.

The id attribute is a unique string or integer value, just likea primary key in a relational database. This id attribute is useful when retrieving the model from a collection, and it is also used to form part of the URL for the model.

The cid attribute is generated automatically by Backbone when the model is first created; it can be used to serve as a unique identifier when the model has not yet been saved to the server and does not have its real ID available.

Sometimes the model you are retrieving from the backend will use a different unique key. For example, the server might use an ISBN as the unique identifier for a book, or a user IDfield might be the identifier used for a User model when saved. The idAttribute attribute allows you to provide a mapping between that key to the ID in your model, meaning that the server will use that attribute to populate the ID.

Saving Models

The save function invokes the operation to save the model to the server, invoking the Backbone.sync function. We’ll see later how the sync function can be replaced to provide alternative means of persisting models, but for now we’ll follow the default behavior that’s already built into Backbone.

Now that we have a back-end service to hook into, we can set the urlRoot attribute of the Model object.

Book = Backbone.Model.extend({
        urlRoot: 'http://localhost:8080/books/',

This URL tells Backbone where to point when performing any operations that require responses from a back-end service. As our node.js server is running on port 8080 and we’ve set up endpoints to respond from /books/, our URL is constructed as in the previous code listing.

In cases where the id attribute has not been set and save is called, the model will invoke acreate operation (HTTP POST) on the back-end REST service, while an update (HTTP PUT) operation will be used when the ID has been specified. This is a simple way of ensuring that a single save function can be used regardless of whether your model has beennewly createdor has been edited since last retrieved from the server.

The save function can be called with no parameters or can take the set of attributes you want to persist to the server, along with an options hash that contains handlers for both success and error cases.

thisBook.save(thisBook.attributes,
{
       success: function(model, response, options){
               console.log('Model saved'),
               console.log('Id: '  +thisBook.get('id'));
       },
       error: function(model, xhr, options){
               console.log('Failed to save model'),
       }
});

Success and error handlers are important when making calls to remote API endpoints, and you cannot be certain that a call to save a model will always be successful. Once the call is complete and has returned, the appropriate callback will be invoked, either success or error. Don’t forget that calls are made asynchronously, so any lines of code after the save method won’t wait for the save to be completed first.

The save method will have invoked the following piece of code served up by the Node.js server:

app.post('/books/', function (request, response) {
   response.header('Access-Control-Allow-Origin', '*'),

   var book = request.body;
   console.log('Saving book with the following structure ' + JSON.stringify(book));
   book.id = bookId++;
   response.send(book);

});

The most important part of this function is that it provided a new ID for the book and returned the new structure of the book object in JSON format. Now that this book object ID assigned to it, any subsequent save methods will invoke the update operation on the backend.

Remember that, if specified, the validation function will be called during the execution of save(). If the validation fails, the model will not be sent to the server.

Retrieving Models

If you want to reset the state of your model object to the same as it is on the server side, you can invoke the fetch() function. Again, this function accepts an options hash that includes success and error callbacks.

thisBook.fetch({
success: function(model, response, options){
       console.log('Fetch success'),
},
error: function(model, response, options){
       console.log('Fetch error'),
}
});

If the execution of the fetch function detects that there is a difference in the models between the server and client sides, a change event will be triggered. This can be useful when you want to ensure that the application is in sync with the back-end service or when you need to populate your model objects on application start-up.

Deleting Models

The final server operation that you may want to carry out is a delete operation to remove the model from the backend.

thisBook.destroy({
               success: function(model, response, options){
                      console.log('Destroy success'),
               },
               error: function(model, response, options){
                      console.log('Destroy error'),
               },
               wait: true
               });

If the model is new and doesn’t yet exist on the server, the destroy operation will fail. Adding a wait:true option will ensure that the model is successfully removed from the server before it is removed from any Backbone.Collection that contains it on the client side.

Parsing Server Responses

When invoking the save() or fetch() function, you may want to parse the model to enrich your data model by adding additional attributes or removing unnecessary attributes. You can do this by adding a parse() function to your model definition. The following example shows how to add a new attribute to your data model after a save or fetch operation:

Book = Backbone.Model.extend({
       parse: function(response, xhr) {
               response.bookType = 'ebook';
               return response;
        }
});

Note that as well as adding new attributes, you could make other changes to make the data returned from the API work for your front-end application, such as changing the currency of an attribute.

Extending Your Own Models

As in any object-oriented language, you will sometimes want to create a hierarchy within your model layer. One of the great things about Backbone models is the extend mechanism, which you can use to extend your own models.

As you’ll recall, when creating new models, you need to use Backbone.Model.extend in the creation.

Book = Backbone.Model.extend({

You can use the same format to extend this further. In the followingexample, let’s create an EBook model, extending the original Book model with an additional method:

EBook = Book.extend({
        getWebLink: function(){
        return 'http://www.apress.com/'+this.get('name'),
        }
});

var ebook = new EBook({name: "Beginning Backbone", author: "James Sugrue"});
console.log(ebook.getWebLink());

To call a function in the parent class, you need to call it explicitly using the prototype and passing through this so that the current model is used as the context.

EBook = Book.extend({
        printDetails: function(){
               console.log('An ebook'),
               Book.prototype.printDetails.call(this);
        }
});

The previous code results in the printDetails function from the Book model being executed after an additional print statement defined in the EBook model is displayed. If there were any parameters involved in this function, they would be passed along after the this reference.

We’ll see later that there are some Backbone plug-ins available that make this call a little simpler.

Backbone Collections

In the previous section we focused on the single models, but usually Backbone applications use Backbone.Collection to provide ordered sets of models. This has some useful side effects, such as being able to fetch an entire collection from a back-end server and listening for events across any of the models in a collection.

When defining a collection, you will always pass through the model that is being contained. To follow on with the book example used in the Backbone.Model section, we will define our collection of books as a library.

//Define a collection based on book
var Library = Backbone.Collection.extend({model: Book});

Just as with Model, an initialize method can be provided in your definition of the collection, which can be invoked on construction of a new instance of the collection. This can be useful to set up event listeners for the collection.

var Library = Backbone.Collection.extend({model: Book,
       initialize: function(){
               console.log('Creating a new library collection'),
       }
});

When a new Library object is created, you’ll see the console.log statement from the initialize function print out to the console.

var myLibrary = new Library();

Constructors

When creating an instance of the collection, you can pass through an array of model objects to populate it with some initial content.

var bookOne = new Book({name: 'Beginning Backbone', author: 'James Sugrue'});
var bookTwo = new Book({name: 'Pro Javascript Design Patterns', author:'Dustin Diaz, Ross Harmes'});

var myLibrary = new Library([bookOne, bookTwo]);
console.log('Library contains ' + myLibrary.length + ' books'),

The .length property allows you to get the number of models currently contained within the collection. Note that the .size() function returns the same number.

Manipulating Collections

As the collection is used throughout the lifecycle of an app, the contents are bound to change, so you will need to manipulate your collection after construction. While it is possible to use the .models property to get a raw array of the models contained, there are more useful utility methods available to manipulate the collection.

Adding Models

Adding a new model to your collection can be easily done using the .add method.

var bookThree = new Book({name: 'Pro Node.js for Developers', author: 'Colin J. Ihrig'});
myLibrary.add(bookThree);
console.log('Library contains ' + myLibrary.length + ' books'),

The .add method will also accept an array of books.

var bookFour = new Book({name: 'Pro jQuery', author: 'Adam Freeman'});
var bookFive = new Book({name : 'Pro Javascript Performance', author: 'Tom Barker'});
myLibrary.add([bookFour, bookFive]);console.log('Library has ' + myLibrary.length + ' books'),

If a model is already present in the collection when passed through to .add, it will be ignored. However, if {merge: true} is included in the call, the attributes will be merged into the duplicate model.

myLibrary.add(bookOne, {merge:true});console.log('Library has ' + myLibrary.length + ' books'),

Note that an add event is fired when models are added to the collection.

You can also use the .push() function to add a model to the end of the collection, providing either an array or a single model, as in the .add function.

myLibrary.push(bookFive);console.log('Library has '+ myLibrary.length + 'books'),

To do the opposite of .push()and add the model to the beginning of the collection, use the .unshift function.

myLibrary.unshift(bookFive);
console.log('Library has '+ myLibrary.length + ' books'),

Removing Models

As you’d expect, a .remove function is available for removing a single modelor array of models.

myLibrary.remove(bookFive);
console.log('Library contains ' + myLibrary.length + ' books'),
myLibrary.remove([bookThree, bookFour]);
console.log('Library contains ' + myLibrary.length + ' books'),

A remove event is fired when models are removed. An options object used in the listener can access the index of the element that has been removed.

var Library = Backbone.Collection.extend({model: Book,
       initialize: function(){
               this.on("remove", function(removedModel, models, options){
                      console.log('element removed  at ' + options.index);
               });

       }
});

The .pop() function removes and returns the last model in the collection.

var lastModel = myLibrary.pop();

To remove the first model in the collection, use the .shift() function rather than .pop(). This will also return the model you are removing.

var firstModel = myLibrary.shift();

Resetting Collections

The reset function exists to provide the ability to replace the set of models in a collection in a single call.

myLibrary.reset([bookOne]);
console.log('Library contains ' + myLibrary.length + ' books'),

You can empty the collection in one go by calling the reset method with no parameters.

myLibrary.reset();
console.log('Library contains ' + myLibrary.length + ' books'),

Using reset fires a single reset event rather than a sequence of remove and add events. This is useful for performance reasons because your application needs to react just once to a reset operation.

Smart Updating of Collections

The set function is described by the official Backbone documentation as a way to perform smart updates on a collection. By passing through an array of models, set abides by the following rules:

  • If a model doesn’t yet exist in the collection, it will be added. The rule will be ignored if {add: false} is provided.
  • If a model is already in the collection, the attributes will be merged. The rule will be ignored if {merge: false} is provided.
  • If there is a model in the collection that isn’t in the array, it will be removed. The rule will be ignore if {remove: false} is provided.

The following code snippet shows how the remove rule can be ignored:

myLibrary = new Library([bookOne, bookTwo]);
console.log('Library contains ' + myLibrary.length + ' books'),
myLibrary.set([bookTwo], {remove: false});
console.log('Library contains ' + myLibrary.length + ' books'),

Without {remove:false}, the result of the second evaluation of myLibrary.length would have been 1.

Traversing Collections

A number of functions are available to run on your collections in order to retrieve individual models and iterate over the entire collection. The following section goes through these in detail.

Retrieving Models

Provided that you know the id of your models, you can retrieve a model from a collection using the .get function. Recall that until a model has been synchronized with a back-end service, the cid attribute is used instead.

var aBook = myLibrary.get('c5'),              console.log('Retrieved book named ' + aBook.get('name'):

If no model matches the id or cid that is used as a parameter, this function returns undefined.

If you don’t want to use IDs, you can also use the .at function, which accepts the index at which the model is present in the collection. If the collection is not sorted, the index parameter will refer to the insertion order.

varanotherBook = myLibrary.at(1);      console.log('Retrieved book named ' + anotherBook.get('name'):

Iterating Through Collections

Although you can use a simple for loop to iterate through your collection as follows:

//a simple loop
for(var i = 0; i < myLibrary.length; i++){
       var model = myLibrary.at(i);
       console.log('Book ' + i + ' is called ' + model.get('name'));
}

there is a more elegant utility function provided by Underscore that helps iterate through collections, namely, the forEach function.

//using forEach
myLibrary.forEach(function(model){
       console.log('Book is called ' + model.get('name'));
});

Other Utility Methods

Making the most of Backbone’s dependency on Underscore, Backbone.Collection has a number of utility methods available. Some of these have already been listed in previous sections. Let’s explore the remaining functions.

Sorting Collections

Using the sortBy function, you can choose an attribute to use as a basis to sort your collection.

//sort collection
var sortedByName = myLibrary.sortBy(function (book) {
   return book.get("name");
});
console.log("Sorted Version:");

sortedByName.forEach(function(model){
       console.log('Book is called ' + model.get('name'));
});

Note that sortBy returns a sorted array representation of the models in the collectionand doesn’t actually change the order of the models within the collection.

By utilizing comparators, you can impose a sorted order that will always be used for your collection. It’s probably most useful to define your comparator function during collection definition. The following example illustrates how to create a comparator to order books by name:

var Library = Backbone.Collection.extend({model: Book,
initialize: function(){
        //initialize function content..
},
comparator:  function(a, b) {
         return a.get('name') < b.get('name') ? -1 : 1;
}
  });

The collection will now always be sorted by name. However, if you change an attribute value in one model, the collection will not rearrange the ordering. Instead, the order can be applied by invoking the sort function for the collection.

myLibrary.at(0).set('name', 'Z'),
myLibrary.forEach(function(model){
       console.log('Book is called ' + model.get('name'));
});
//force sort
myLibrary.sort();
myLibrary.forEach(function(model){
       console.log('Book is called ' + model.get('name'));
});

Note that the sorting order will be reapplied when a new model is added to the collection, unless the {sort: false} option is passed through when adding.

Shuffle

If you need to get a randomized version of the models in your collection, the shuffle() function will return an array of the models that have had a shuffling algorithm applied.

varshuffled = myLibrary.shuffle();

Iterating through the collection now will present the books in a different order than before.

myLibrary.forEach(function(model){
       console.log('Book is called ' + model.get('name'));
});

Getting a List of Attributes

Sometimes you may want to get a list of all the instances of a particular attribute in your collection. This can be done easily using the .pluck() function.

console.log('List of authors in collection:'),
var authors = myLibrary.pluck('author'),
authors.forEach(function(authorName){
       console.log(authorName)
});

Searching

A number of useful search mechanisms are available, based on passing through a set of key-value pairs to search functions.

For example, to get an array of model objects that match a certain criteria, use the .where function.

var results = myLibrary.where({author:'James Sugrue'});
console.log('Found: ' + results.length + ' matches'),

The findWhere function can be used to find the first model that matches the query, rather than returning an array of matches.

var result = myLibrary.findWhere({author:'James Sugrue'});
console.log('Result: ' + result.get('author'));

You can also group model objects by common attribute values. For example, if each of the books had a Boolean indicating whether they were published, we could group them as follows:

var byPublished = myLibrary.groupBy('published'),
myLibrary.forEach(function(model){
        console.log('Book is called ' + model.get('name'));
});

There are a lot more utility methods available to Collections, which you can read about at http://backbonejs.org. However, those listed here should be enough to get you started.

Exchanging Data with the Server

To wrap up our overview of Collections, we will take a look at how collections interact with the server. In the first half of the book, when looking at Backbone.Model, we saw how individual models could be retrieved, saved, updated, and retrieved using a REST API running on another server.

Collections can help organize this in a more efficient way, by managingall modelsand providing utilities to make saving models to the server a little easier.

Setup

The first thing you need to do is tell the collection where it needs to point to for its data persistence, using the url attribute. We’ll be pointing to our Node.js server, running a /books/ API endpoint on port 8080.

var Library = Backbone.Collection.extend({model: Book,
        url: 'http://localhost:8080/books/',

        initialize: function(){
        ....

Retrieving Data from the Server

Just as with Backbone.Model, collections have a .fetch method that is used to retrieve the collection data from the server.

var myOnlineLibrary = new Library();

myOnlineLibrary.fetch({
               success: function(e){
                      console.log('Got data'),
               },
               error: function(e){
                      console.log('Something went wrong'),
               }
});

As with all functions that deal with online operations, you can provide success and error callbacks to detect whether the operation completed correctly.

Similar to Backbone.Model, a parse method can be defined at the collection level, which allows you to customize the data returned from the server. The response parameter contains an array of all model objects that are retrieved after the execution of .fetch.

var Library = Backbone.Collection.extend({model: Book,
               ......
       parse: function(response, xhr) {
               //customisations here
               return response;
        },
});

Saving Data to the Server

While retrieving data is done in a batch, saving models to the server is still done onan individual basis, as in the Backbone.Model sections of this chapter.

//add a model to the collection and save to server
myOnlineLibrary.add(bookOne);
bookOne.save({
               success: function(e){
                      console.log('Book saved to server'),
               },
               error: function(e){
                      console.log('Error saving book);
               }
});

However, if you use the Backbone.Collection.create function rather than .add, the model is both added to the collection and persisted to the server.

//add to the collection and save all at once
myOnlineLibrary.create(bookTwo);

Deleting Data from the Server

Removing models from the server is done at the individual model level using the .destroy method, as described in the previous section.

bookOne.destroy({
                             success: function(e){
                                     console.log('Book deleted')
                             },
                             error: function(e){
                                     console.log('Error deleting book'),
                             }
                      });

Collection Quick Reference

This section provides an overview of the functions, properties, and events that are related to Backbone.Collection.

Collection Method Reference

As you can see, there are a number of useful functions available for dealing with collections in Backbone. Table 3-3 describesthem for quick reference.

Table 3-3. Methods Available for Collections

Function/Property

Description

.length.size()

Returns the number of model objects contained within the collection

.models

Returns a raw array of all the model objects

.add([models])

Adds a single model, or an array of models, to the collection

.push([models])

Adds a single model, or an array of models, to the end of the collection

.unshift([models])

Adds a single model, or an array of models, to the start of the collection

.remove([models])

Removes a single model, or an array of models, from the collection

.pop()

Removes and returns the model at the end of the collection

.shift()

Removes and returns the model at the beginning of the collection

.reset()

Replaces the models contained in a collection or empties the collection by calling with no parameters

.get(id)

Retrieves a model from the collection using the id or cid as the parameter

.at(index)

Retrieves a model at a particular index from the collection

.foreach(function)

Iterates through each model in a collection

sortBy()

Returns an array of models from the collection, sorted by a particular attribute

.sort()

Reapplies the sorting order on a collection

.where({key:value})

Finds all models within the collection that have the specified key-value

.findWhere({key:value})

Finds the first instance of a model that has the specified key-value

.url

Finds the location at which to point to for the REST API endpoint

.fetch()

Retrieves the entire collection from the server

.create(<model>)

Adds a new model to the collection and saves to the server

Collection Event Reference

Just like models, collections have a number of events that get fired when the contents change. Table 3-4 lists them for quick reference.

Table 3-4. Events Related to Backbone Collections

Function/Property

Description

add

Detects when a model has been added to the collection

remove

Detects when a model has been removed from a collection

reset

Detects that the collection has been reset

sort

Detects that the collection is being sorted

change

Detects that a model within the collection is being changed.

change:<attribute name>

Detects that <attribute name>, a model within the collection, is being changed

Summary

This chapter focused on the data layer of a Backbone application, with the first section describing the functionality available to Model objects and the second half dealing with collections. We saw how collections neatly encompass Model objects; most importantly, we learned about the interactions between a server that exposes a REST API running on a Node.js server and a Backbone client application.

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

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