ruk·si

🦴 Backbone Guide

Updated at 2013-11-08 15:24

This guide contains basic syntax and tips how to write Backbone code.

Backbone is a JavaScript library that provides structure to web applications. It is not a framework, but a collection of components that you can use how you like.

Backbone tries to fix major problems in normal JavaScript development:

  • Problem: data is generation at server --> send to client --> saved to DOM.
  • Backbone saves data into memory using JavaScript and what is shown is only one view to the data.

Main benefits of Backbone:

  • Allows client-side application structure.
  • Provides models to store data.
  • Provides collections to group models.
  • Provides views to hook up with models and collections.
  • Good support for custom events.
  • Synchronizes data to and from server.

This document is an overview how components can be used. If you like Backbone, you should also check Marionette and Thorax that offer more components on top of Backbone e.g. Layout, ItemView.

Basics of Backbone

Never read things from the DOM. Save everything on Backbone models. You should never need to read the class of a DOM element. Only view rendering is allowed to touch the DOM.

It is ok to use small modules or libraries outside of Backbone that handles general functionality.

Learn Backbone dependency underscore really well. It will help you eliminate any loops in your code.

var filteredNames = collection.chain()
    .filter(function(item) { return item.get('age') > 10; })
    .map(function(item) { return item.get('name'); })
    .value();

All Backbone prototypes have initialize constructor.

var Todo = Backbone.Model.extend({
    initialize: function() {
        console.log('This model has been initialized.');
    }
});

Backbone Models

Models are meant to reflect data and logic on your server, but not all of it. Never repeat yourself.

Models are place to store your data.

// Creating a behaviourless model prototype.
var Item = Backbone.Model.extend({});

// Creating and adding some values to a model.
var item = new Item({
    description: 'Pickup milk',
    status: 'incomplete',
    id: 1
});

// Getting a stored value.
item.get('description');

// Changing a stored value.
item.set('status', 'complete');
item.set({status: 'complete'});

You can specify default values to models that are used if nothing else is specified.

var Item = Backbone.Model.extend({
    defaults: {
        description: 'Empty todo...',
        status: 'incomplete',
        tag: null
    }
});

You should specify model identifier attribute if you have your own unique identifier for the models.

var Item = Backbone.Model.extend({
    idAttribute: 'item_number'
});
// item.id is 1 if data where model is constructed has item_number as 1.

Backbone provides default model synchronization that assumes a RESTful endpoint. You can overwrite the functionality if you wish.

// Model can have access URL to enable syncronization.
var Item = Backbone.Model.extend({
    url: '/items'
});

// Populate or updates given model's attributes according to its ID.
// By default: GET to /items/1
var item = new Item({ id: 1 });
item.fetch();

// Updates given model's attributes to database.
// By default: PUT to /items/1 with JSON parameters
var item = new Item({ id: 1 });
item.save();

// Remove given model from database.
// By default: DELETE to /items/1
var item = new Item({ id: 1 });
item.destroy();

// Creates new model to the database.
// By default: POST to /items with JSON parameters
var item = new Item({name: 'Banana'});
item.save();

// Simple example for custom implementation.
var Item = Backbone.Model.extend({
    sync: function(method, model, options) {
        if (method === 'create') {
            // ...
        }
        else if (method === 'read') {
            // ...
        }
        else if (method === 'update') {
            // ...
        }
        else if (method === 'delete') {
            // ...
        }
    }
});

You can add extra parameters to fetch using data option.

// If we want to call GET /todos?page=6
todoItems.fetch({data: { page: 6 }});

You can get model values as an object. But changing values this way does not trigger any events.

todoItem.attributes;

You can escape user input on the model.

todoItem.escape('description');

You can bind events to models. Note that view related events like 'render' should be done on the view side.

// 'change' is fired when any of values change.
todoItem.on('change', aFunctionToRun);
todoItem.trigger('change');
todoItem.off('change', aFunctionToRun);

Models have support for validation.

var Todo = Backbone.Model.extend({
  defaults: {
    completed: false
  },

  validate: function(attributes){
    if (attributes.title === undefined) {
        return "Remember to set a title for your todo.";
    }
  },

  initialize: function() {
    this.on("invalid", function(model, error) {
        console.log(error);
    });
  }
});

var myTodo = new Todo();
myTodo.set('completed', true, {validate: true});
console.log('completed: ' + myTodo.get('completed'));
// => completed: false

Backbone Views

Views are components for displaying a model, a collection or other data.

// Initialize without a model.
var todoView = new TodoView();

// Initialize with a model.
var todoView = new TodoView({
    model: todoItem
});
// todoView.model === todoItem;

// Initialize with a collection.
var todoView = new TodoView({
    collection: todoItems
});
// todoView.collection === todoItems

// Custom options on initialization.
var todoView = new TodoView({
    model: todoItem,
    user: currentUser
});
// todoView.options === { user: currentUser }

// Accessing custom options on initialize.
// This way custom options are easier to access later.
var TodoView = Backbone.View.extend({

    user: null, // Default value.

    initialize: function(options) {
        _.defaults(options || (options = {}), {
            user: this.user,
        });
        this.user = options.user;
    }

});

Rendering should empty the container and insert template in it again. Rendering a view repeatedly should not break it.

Every view has a container element el. It can also be accessed with $el which is a jQuery or Zepto wrapped version. You can change the view container element tag type with tagName. This will change what the container element el is. By default, it is a div without id or class.

var TodoView = Backbone.View.extend({
    tagName: 'li',
    id: 'myId',
    className: 'todo'
});

You can change the container element el manually with setElement.

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

var View = Backbone.View.extend({
    events: {
        click: function(e) {
            console.log('Clikky!');
        }
    }
});

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

button1.trigger('click'); // => Clikky!
button2.trigger('click');

view.setElement(button2);

button1.trigger('click');
button2.trigger('click'); // => Clikky!

You should name your render function render.

var TodoView = Backbone.View({
    render: function() {
        var html = '<p>';
        html += this.model.get('description');
        html += '</p>';
        this.$el.html(html);
        return this;
    }
});

var todoItem = new TodoItem({ id: 1 });
todoItem.fetch({

    // Run if fetch is a success.
    success: function() {

        // Associate a new view with the model.
        var todoView = new TodoView({ model: todoItem});

        // Render the view.
        todoView.render()

        // Place the view to the DOM.
        $('#todo-list-container').append(todoView.$el);
    }
});

You can also use existing DOM elements for the container element.

var todoView = new TodoView({
    model: todoItem,
    el: $('.todo')
});

You can handle events as with models with on, off and trigger. But you can also use events object in the view.

var TodoView = Backbone.View.extend({

    // Format is {'event selector': 'callback'}.
    events: {
        'click h3': 'alertStatus'
    },

    alertStatus: function(e) {
        e.preventDefault();
        alert('yo!');
    }

});

Views use underscore templating by default.

var AppointmentView = Backbone.View.extend({

    template: _.template('<span><%= title %></span>'),

    render: function(){
        var attributes = this.model.toJSON();
        this.$el.html( this.template(attributes) );
    }

});

Personally I use Mustache for templating because it works also in server side so you can even prerender views on server side. Handlebars is also very good.

var TodoItemView = Backbone.View.extend({

    template: Mustache.compile(
        '<span>{{description}}</span'
        + '<em>{{assignedTo}}</em>'
    ),

    render: function(){
        this.$el.html( this.template(this.model.toJSON()) );
    }

});

Events

Backbone components have many events built-in. Go and read them through online so you can plan your events right.

var logFunc = function() { console.log('Handled Backbone event'); };
model.on('event', logFunc);
model.trigger('event');
model.off('event');
model.trigger('event');
ourObject.on("all", function(eventName){
  console.log("The name of the event passed was " + eventName);
});

// This time each event will be caught with a catch 'all' event listener
ourObject.trigger("dance:tap", "tap dancing. Yeah!");
ourObject.trigger("dance:break", "break dancing. Yeah!");
ourObject.trigger("dance", "break dancing. Yeah!");

You should avoid event handlers outside components when using Backbone. But sometimes they are necessary.

// bad
App.PhotoView = Backbone.View.extend({
    // ...
});
$('a.photo').click(function() {
    // ...
});

// good
App.PhotoView = Backbone.View.extend({
    events: {
        'click a.photo': function() {
            // ...
        }
    }
});

You can also run actions without triggering events by passing option silent: true.

todoItem.set({status: 'complete'}, {silent: true});

Collections

Collection is a group of models.

var TodoList = Backbone.Collection.extend({
    model: TodoItem
});
var todoList = new TodoList();

todoList.length;
todoList.add(todoItem);
todoList.remove(todoItem);
todoList.at(1);
todoList.get('1e4a5d8');

You can populate the collection on client side or on server side.

// Populating on the client side.
var todos = [
    {description: 'Pick up milk.', status: 'incomplete'},
    {description: 'Get a car wash', status: 'incomplete'},
    {description: 'Learn Backbone', status: 'incomplete'}
];
todoList.reset(todos);

// Populating from the server side.
var TodoList = Backbone.Collection.extend({
    url: '/todos'
});
// Default: GET to /todos
todoList.fetch();

You can easily iterate through all models in a collection.

// Iterating, non-changing
todoList.forEach(function(todoItem) {
    alert( todoItem.get('description') );
});

// Iteration, changing the resulting array
var titles = todoList.map(function(todoItem) {
    return todoItem.get('title');
});

You can specify custom filtering and sorting for a collection.

// Filtering collection.
var incompletes = todoList.filter( function(todoItem) {
    return (todoItem.get('status') === 'incomplete');
});

// Sort with default comparator function using specific attribute.
var TodoItems = Backbone.Collection.extend({
    comparator: 'status'
});

// Use custom comparator function.
var TodoItems = Backbone.Collection.extend({
    comparator: function(todoA, todoB) {
        return (todoA.get('status') < todoB.get('status'));
    }
});

You can specify functions to return aggregate values.

var TodoItems = Backbone.Collection.extend({
    completedCount: function() {
        return this.where({status: 'complete'}).length;
    }
});
todoItems.completedCount();

View for collections works a lot like views for models.

var TodoListView = Backbone.View.extend({

    paginationTemplate: _.template(
        '<a href='#/todos/p<%= page %>'>next page</a>'
    ),

    initialize: function() {

        // Listen if the collection changes.
        this.listenTo(this.collection, 'reset sync', this.render);
    },

    render: function(){
        this.$el.empty();
        this._renderAll();
        this._renderPagination();
        return this;
    },

    _renderAll: function() {
        this.collection.forEach(this._renderOne);
    },

    _renderOne: function(todoItem) {
        var todoView = new TodoView({ model: todoItem });
        this.$el.append( todoView.render().el );
    },

    _renderPagination: function() {
        this.$el.append(
            this.paginationTemplate({page: this.collection.page + 1})
        );
    }

});

var todoListView = new TodoListView({
    collection: todoList
});
todoListView.render();

Parsing

Parse is function that is run on the result set after fetch.

// When you call fetch, it will parse the response.
todoItem.fetch();

// Default parse looks like this.
var TodoItem = Backbone.Model.extend({
    parse: function(response) {
        return response;
    }
});

// If our response looks like this.
{todo: {id: 1, desc: 'Pick up milk', status: 'incomplete'} }

// You might want to parse it like this.
var TodoItem = Backbone.Model.extend({
    parse: function(response) {
        response = response.todo;
        response.description = response.desc;
        delete response.desc;
        return response;
    }
});

You can force the parse.

var todoItem = new TodoItem({
    todo:{id: 1, desc: 'Pick up milk', status: 'incomplete'}
}, { parse: true });

You can also overriding collection parse to save metadata about the collection.

// If our format from server is following...
// {
//     'total': 25,
//     'per_page': 10,
//     'page': 2,
//     'todos': [ {'id': 1, ... }, {'id': 2, ... } ]
// }

var TodoItems = Backbone.Collection.extend({
    parse: function(response) {
        this._perPage = response.per_page;
        this._page = response.page;
        this._total = response.total;
        return response.todos;
    }
});

toJSON is used to serialize the model for storage.

// Default
var TodoItem = Backbone.Model.extend({
    toJSON: function() {
        return _.clone(this.attributes);
    }
});

You might need to customize toJSON.

// Overridden should look something like this.
var TodoItem = Backbone.Model.extend({
    toJSON: function() {
        var attrs = _.clone(this.attributes);
        attrs.desc = attrs.description;
        attrs = _.pick(attrs, 'desc', 'status');
        return { todo: attrs };
    }
});

Router

Routers are objects that map URLs to actions.

var TodoRouter = new Backbone.Router.extend({

    routes: {
        'todos': 'startApplication',
        'todos/:id': 'showItem'
    },

    intialize: function( options ) {
        this.todoList = options.todoList;
    },

    startApplication: function() {
        // Is called on /todos or #todos
        this.todoList.fetch();
    },

    showItem: function(id) {
        // Is called on e.g. /todos/123
        this.todoList.focusOnTodoItem(id);
    }

});

Route syntax allows specifying parameters for the target actions.

'search/:query/p:page'      Parameters are query and page.
'folder/:name-:mode'        Parameters are name and mode.
'file/*path'                Parameter is path, everything after file/.
'search/:query(/p:page)(/)' Parameter page is optional.
'*path'                     Catches all.

You must initialize Backbone.history as it handles URL hash changes. Prefer HTML5 pushstate over using hashtags. Check how it works in IE8 and IE9.

// Modern browsers support cleaner URLs.
Backbone.history.start({ pushState: true });
// Old browsers are stuck with # syntax.
Backbone.history.start();

You can change the current route.

// This will just update the URL.
router.navigate('todo/1');
// This will also launch all router events.
router.navigate('todo', { trigger: true });

You can use regular expressions in routes.

// Regex routes need to defined inside initialize because
// regex object cannot be an object key.
var TodoRouter = new (Backbone.Router.extend({
    initialize: function() {
        this.route(/^todos\/(\d+)$/, 'show');
    },
    show: function(id) {
        console.log('id = ' + id);
    }
}));

Examle how to use router in application structure.

Router routes URLs to layouts.
    -> Layouts contain a set of views.
        -> Views may load and unload other views it contains.
            -> Views know what data they need to render e.g. template.
                -> Data is shared between all views.

Sources