fbpx logo-new mail facebook Dribble Social Icon Linkedin Social Icon Twitter Social Icon Github Social Icon Instagram Social Icon Arrow_element diagonal-decor rectangle-decor search arrow circle-flat
Development

Backbone.js with a Spine – Part 2: Models and Collections

Joe Hirn Tandem Alum

last updated June 19, 2013

In part 1 of this series I took it a little light by introducing the App object. That was in no way specific to Backbone but I wanted to cover it as we’ll be seeing it in future posts.

Today I’d like to dive into some specific Backbone functionality. They’re the heart and soul of your application. I’m talking of course about models and collections.

Avoiding some boilerplate

While we have a couple of strays, there’s pretty much a one-to-one relationship between our Rails and Backbone models. We also create a collection for most models. Because models and collections are so closely related, we like to keep them in the same file. An example of such a file might look like this:

// In app/assets/javascripts/models/user.js

App.Models.User = Backbone.Model.extend({
  urlRoot: '/url-goes-here'
});

App.Collections.Users = Backbone.Collection.extend({
  url: '/url-goes-here',
  model: App.Models.User
});

As you can see our namespace in App is plural as many models and collections live there. We also pluralize our model name for the collection name because it just makes sense. Because this is so boiler plate, keystroke conservationist Dayton Nolan created a Rails generator for us to use. Invoking rails g bmodel user does three things:

  • Creates the file in app/assets/javascripts/models/user.js
  • Copies the require statement //= require models/user you need to add to the application manifest for the asset pipeline
  • Opens application.js with whatever EDITOR you have set so you can paste it in.

The last part is probably the most useful. As you’re about to see, we reference other models in our models which means require_tree doesn’t give us the control over load order we need for the JavaScript manifest. It’s pretty easy to forget to add this require for each individual file, but adding it automatically was something we really couldn’t do either. So having it open the file with what you need to add ready to paste is about as close to automation as we could get.

Beware the defaults

Although the documentation for defaults has a note on this, it may be worth discussing an example of what can happen if you’re looking to use an object for one of your default values. It makes sense because JavaScript passes objects by reference, but it can manifest itself as a serious wtf.

Let’s take a model with the following defaults:

App.Models.Team = Backbone.Model.extend({
  defaults: {
    roster: new App.Collections.Players
  }
});

When we create a new instance of Team the attributes are set to the value of the defaults. But the value of roster is a reference to a single instance of Players. What this means is that it is shared across all instances of Team and can cause the following unexpected behavior.

var team1 = new App.Models.Team;
var team2 = new App.Models.Team;
team1.get('roster').add(new App.Models.Player);
team1.get('roster').size(); // => 1, That's fine
team2.get('roster').size(); // => 1, That's totally not fine
(new App.Models.Team).get('roster').size(); // => 1, That's just weird

Well, it’s weird, but there’s a way to get around it. By wrapping the defaults in a function, we get a fresh copy with every instance.

App.Models.Team = Backbone.Model.extend({

  defaults: function(){
     return {roster: new App.Collections.Players};
  }

});

var team1 = new App.Models.Team;
var team2 = new App.Models.Team;
team1.get('roster').add(new App.Models.Player);
team1.get('roster').size(); // => 1, That's fine
team2.get('roster').size(); // => 0, Yes!
(new App.Models.Team).get('roster').size(); // => 0 Expected!

All is saved by a friendly function =).

Models all the way down

When creating a new model it is possible to create it with nested attributes. The only problem is the nested attributes remain as primitive objects rather than the Backbone models which represent them. What we wanted was a way to box these primitives automatically.

The first thing we did was write a function on Model which allowed us to easily wrap the attributes by passing the name of the attribute and the type we wanted to wrap it with. Then we wanted a way to ensure things were automatically wrapped for us. It turns out there are three places where we need this to happen: initialization, fetch, and (because it updates attributes on the model with the response) save. Parse is a great method to hook into for manipulating attributes on fetch and save and it turns out passing the option {parse: true} to initialize it will also call parse during initialize.

We found the least invasive way to do this was to create an App.Models.BaseModel which extends Backbone.Model and use it as the base model for all of our classes. This model provides an empty function preParse which is called by its implementation of parse. It also automatically passes {parse: true} to initialize to ensure we are parsing attributes during initialization. If we need to wrap any of the primitive attributes, you can provide an implementation of preParse in your model and the rest is taken care of.

Here is what our BaseModel looks like and how we might use it in the Player and Team models above:

App.Models.BaseModel = Backbone.Model.extend({

  preParse: function(data){},

  constructor: function(attributes, options){
    options = options || {};
    options.parse = true;
    Backbone.Model.call(this, attributes, options);
  },

  wrapAttribute: function(attributes, key, backboneType) {
    var value = attributes[key];
    if (attributes[key] && isNotTypeOf(backboneType, value)) {
      attributes[key] = new backboneType(value);
    }
  },

  parse: function(data, options){
    this.preParse(data);
    return data;
  }

});

App.Models.Team = App.Models.BaseModel.extend({

  preParse: function(data){
    this.wrapAttribute(data,'roster',App.Collections.Players);
  }

});

App.Models.Player = App.Models.BaseModel.extend({});

App.Collections.Players = Backbone.Collection.extend({
  model: App.Models.Player
});

As you can see the key to subclassing Backbone.Model is by calling extend and implementing the constructor function. Here we can set the parse option and pass it on up to the base constructor of Backbone.Model. Against my preference for immutability, we chose to just have preParse manipulate the attributes as a side effect rather than return a new object. We ensure the attribute is present and not already wrapped by calling isNotTypeOf to prevent it from re-wrapping if it is already wrapped. You may wonder where isNotTypeOf is coming from. You can find the implementation along with other js extensions in this gist..

So now when we instantiate a new team with a roster full of players, each player becomes a full Backbone model that we can pass to a view, bind events on, etc…

var team = new App.Models.Team({
  name: "Bulls",
  city: "Chicago",
  roster: [{name: "Derrick Rose"}, {name: "Joakim Noah"}]
});

var player1 = team.get('roster').first();
player1.get('name'); // => "Derrick Rose"
player1.set('name', "D. Rose");
player1.save();

It should be noted that this also traverses the entire object graph. If Player has a preParse implementation it will be called as the roster is being wrapped with the Players collection and instantiating new players. So if we were to nest more attributes inside of a player, they are also boxed automatically for us. This is very cool! We’ve gotten a lot of mileage out of this. It really makes it easier when returning deeply nested attributes as we frequently find ourselves doing to prevent returning to the server when we need attributes for a related model. Eager loading over round trips FTW (*most of the time)!

There’s so much more to talk about with models but this covers the out of the ordinary situations I felt were worth mentioning. I’m happy to answer questions in comments and write another post if requested. Otherwise I’ll continue on to Views, which will most certainly be more than one post. I’ve got a lot to say about views.

Thanks for reading.

Tandem is innovation firm in Chicago and San Francisco with practice areas in custom software development, mobile and web application development.

Let’s do something great together

We do our best work in close collaboration with our clients. Let’s find some time for you to chat with a member of our team.

Say Hi