JavaScript Architecture: Backbone.js Models

Updated Aug 11, 2012 to reflect current library versions.

Models contain the data or state of an application. Examples of a model would be a book, car, or customer. A book model would contain typical attributes of a book: title, author, genre, and publisher. A regular JavaScript object could contain this data like so:

var book = {
title: 'A Tale of Two Cities',
author: 'Charles Dickens',
genre: 'historical',
publisher: 'Chapman & Hall'
};

But this will soon present a problem. For illustration purposes let’s say you have view A that shows the book information and a separate view B where the user can change the book information. When the information is changed from view B, view A needs to know about it so it can update to show the new info to the user. Because our regular JavaScript object doesn’t have any way to notify view A of the change, view B would need a reference to view A so view B can call a method on view A telling it that the book object has been updated. Better yet, maybe we give view B event triggering powers and view A could just bind to view B’s events. Either way, these options should be frantically waiving red flags in your skull. Your views would now have to be aware of each other in at least one direction and that’s one direction too many. We want to free our views from the necessity of being aware of each other.

Eventful models

Backbone’s models take advantage of the Backbone.Events observer pattern implementation so we can deal with this appropriately:

var book = new Backbone.Model({
title: 'A Tale of Two Cities',
author: 'Charles Dickens',
genre: 'historical',
publisher: 'Chapman & Hall'
});

book.on('change:genre', function(model, genre) {
// Method 1: Use arguments.
alert('genre for ' + model.get('title') + ' changed to ' + genre);
// Method 2: Use book variable captured in closure.
alert('genre for ' + book.get('title') + ' changed to ' + book.get('genre'));
});

book.set({genre: 'social criticism'});

When we instantiate a Backbone model and pass in the native object containing our book information, the model then wraps these attributes. Attributes can then be retrieved and stored using the get and set methods. You can set brand new attributes, unset old ones, or clear them all if you feel the need. If you try to access the attributes directly (e.g., alert(book.genre);) you won't get what you're expecting because the attributes aren't there. The get and set methods are required to deal with a JavaScript limitation in order to trigger events when attributes are changed.

By using a Backbone model our views can now watch the book directly for when the genre changes. Notice that Backbone automatically took the name of the attribute we changed, genre, prepended it with change:, then triggered an event using the resulting string as the key. By watching for the change:genre event, we can be notified any time the genre attribute is changed.

We can also extend Backbone.Model and set defaults using the defaults hash:

var Book = Backbone.Model.extend({
defaults: {
genre: 'historical'
}
});

var taleOfTwoCities = new Book({
title: 'A Tale of Two Cities',
author: 'Charles Dickens',
publisher: 'Chapman & Hall'
});

var goodEarth = new Book({
title: 'The Good Earth',
author: 'Pearl S. Buck',
publisher: 'John Day'
});

// Alerts "A Tale of Two Cities: historical"
alert(taleOfTwoCities.get('title') + ': ' + taleOfTwoCities.get('genre'));

// Alerts "The Good Earth: historical"
alert(goodEarth.get('title') + ': ' + goodEarth.get('genre'));

Now every book that is instantiated will receive the “historical” genre by default which, of course, can be overridden.

Persistence using a static URL

How do we get data to and from a remote server? Glad you asked. Models have syncing functionality built-in. You don’t have to use it, but it can be really nice. Here’s the simplest form:

var book = new Backbone.Model();
book.url = '/book.php';

var fetchSuccess = function(book) {
alert(book.get('title'));
};

book.fetch({success: fetchSuccess});

In this case, there’s no technical need to even extend Backbone.Model. We just instantiate it, set our endpoint url, and call fetch(). As long as the endpoint returns JSON for a single book, the JSON properties will be merged into our model. The result is a model object that then behaves just as any other Backbone model would. Note that we passed a callback function to fetch() that will be executed after a successful response. We could have also passed an error callback function to handle any issues encountered while communicating with the server.

As far as saving data, here’s how we can save a new book to the server:

var book = new Backbone.Model({
title: 'The Tragedy of the Commons'
});

// Or we could've done this instead of passing it into the constructor:
// book.set({title: 'The Tragedy of the Commons'});

book.url = '/book.php';

var saveSuccess = function(book) {
alert('book saved!');
};

book.save({success: saveSuccess});

In this case, the save() method will send a POST request to the server containing the book data in JSON format. If the model already exists on the server (which by default is deemed true if the model contains an id attribute), the save() method will send a PUT request instead of a POST. The destroy() method will issue a DELETE request.

Persistence using a pattern URL

While a static url may do in some cases (previously /book.php), it's more likely you'll have a url pattern for retrieving your models. For example, to get the book with ID = 2 your endpoint may be http://example.com/books/2. The "books" part of the path is common for all books, but the id on the end varies. In this case we could do this:

var Book = Backbone.Model.extend({
urlRoot: '/books'
})

var myBook = new Book({id: 2});

var fetchSuccess = function() {
alert(myBook.get('title'));
};

// alerts 'using url: /books/2'
alert('using url: ' + myBook.url());
myBook.fetch({success: fetchSuccess});

By creating a Book class and setting the urlRoot property, we can instantiate a book, set the id property, and call fetch(). By default a url will be composed with the pattern we're looking for: [urlRoot]/[id]. You can modify this pattern to suit your particular needs. For example, we could set our model up to have a [urlRoot]?id=[id] pattern. We do this by overriding the url property:

var Book = Backbone.Model.extend({
urlRoot: '/books',
url: function() {
return this.urlRoot + '?id=' + this.get('id');
}
})

Notice in our examples url can be either a property or a method.

Even more likely than dealing with a single book here and there is the need to load and manage a list of books. Fortunately, Backbone helps us there too and we’ll discuss this in the next post in the series.

View state

So far we’ve talked about models in the context of representing entities. In our case, our entity is a book. However, models can also come in handy for storing view state.

Take an application like Photoshop, Illustrator, Lightroom, or pretty much any IDE where there are numerous panels that can be shown or hidden. For illustration purposes, let’s say we’re building a similar app and our requirements are that we have two panels. Panel A happens to show a particular icon on it if and only if panel B is visible. How do we notify panel A when panel B’s visibility is toggled? Suffice it to say we also have a JavaScript object managing each of these panels (one per panel).

If we take the most direct approach, we could pass panel A’s JavaScript object to panel B and when panel B’s visibility is toggled (maybe there’s some sort of hide button within the panel itself) it calls a method on panel A. Or we could just pass a function of panel A’s into panel B. Either way, that just smells bad. Again, we’ve coupled our views. This becomes increasingly problematic as we add more and more panels that need to be aware of each others’ visibility. Maybe due to UXD insanity the requirement changes such that 10 panels have to know about each others’ visibility for one reason or another. And then maybe we have a menu that shows a checkbox next to each of the names of the panels that are visible. And then for memory management purposes we decide to completely destroy and create the panels’ DOM elements and managing JavaScript objects altogether rather than just toggling visibility. Now we have this massive web of object references amongst all the various panels and when we see these new requirements an hour before launch we start to panic. You know this happens in the real world.

Unfortunately, we took the most direct, simple approach now for a lack of flexibility later. This might work in the short term, but it adds to technical debt that may become overwhelming later on.

Instead, what if we created a model to store the state of the view. Maybe we call it our panelState model and the attributes we store on the model might be presetsVisible, histogramVisible, and so on. We can then share this model with any of the panels, the menu, or anything else that might need to know about their state. We can also completely destroy a panel's DOM element and managing JavaScript object when it's hidden and we're okay because rather than every panel having a reference to every other panel, they just have a reference to a single view state model that always exists.

Additional functionality

Additional functionality exists in models including validating, extracting a native object from the Backbone.Model wrapper, determining which attributes have changed, and more.

Optionality and customization

Just because Backbone gives you the ability to do all the things we’ve talked about, rest assured you’re not forced to use any of it. It stays out of the way as much as you want. If you choose, you can write your own function for loading your data wherever you want. You can opt out of using Backbone models completely. Likewise, if there’s functionality that doesn’t fit your system requirements, there are ways to customize the various aspects. I’ll leave that for you as homework whenever you need it, but these are good research techniques to get you headed in the right direction:

Read more in the Backbone.Model documentation.