Backbone.js

static/images/backbone_logo.png
Author:`Jeremy Ashkenas`_
Date:October 13, 2010
Language:JavaScript
License:MIT
Website:http://backbonejs.org/

Backbone.js is a lightweight JavaScript library for making single-page web applications that communicate with a JSON web service built in the Representational State Transfer style. It is based on the model-view-presenter pattern. Backbone by itself is less than 1k lines and around 18KB minified, but requires Underscore.js (14KB) and usually JQuery (93KB). [4]

A useful reference app is the TodoMVC app. http://backbonejs.org/docs/todos.html

Contents

1   Function

What would an ideal client-side API look like? [4] Start with jQuery:

$(".account").each(function() {
    var id = $(this).attr('data-id');
    var data = window.accountJSON[id];
    // ...
    var addresses = $(this).find('.address').length;
    $(this).find('.address_count').text(addresses);
});

Ideally API is something more like:

Accounts.each(function(account) {
    new AccountView({model: account});
});

Sever connection between the data you have and UI have. [4] Once they're separated you can work on each independent without worrying about breaking them.

Models and views are bound together with events. [4]

Events allow you to avoid this:

var changeTitle = function(noteId, title) {
    window.notesJSON[noteID].title = title;
    // Update each place its used
    window.app.toolbar.notes.setTitle(noteId, title);
    window.app.descriptionView.setNoteTitle(title);
    window.app.details.refresh();
};

The business logic; what needs to change when the title changes. But now business logic needs to know about views. Trying to server this kind of coupling. We want:

note.set({title: newTitle});

Where everything listens to changes on the model. If you get rid of anything listening, it doesn't have hooks in the business logic. UI just watches changes and reflects changes as it goes. .

When developing applications using just jQuery, the piece missing is a way to structure and organize your code. It’s very easy to create a JavaScript app that ends up a tangled mess of jQuery selectors and callbacks, all desperately trying to keep data in sync between the HTML for your UI, the logic in your JavaScript, and calls to your API for data.

Without something to help tame the mess, you’re likely to string together a set of independent plugins and libraries to make up the functionality or build everything yourself from scratch and have to maintain it yourself. Backbone solves this problem for you, providing a way to cleanly organize code, separating responsibilities into recognizable pieces that are easy to maintain. [1]

Backbone.js gives structure to web applications by providing models with key-value binding and custom events, collections with a rich API of enumerable functions, views with declarative event handling, and connects it all to your existing API over a RESTful JSON interface.

It is designed for developing single-page web applications, and for keeping various parts of web applications (e.g. multiple clients and the server) synchronized.

2   Substance

A Backbone application is an event-driven application that responds to changes in the state of our models. That is to say we don't want to have our views manipulating themselves and referencing / controlling each other directly. Rather, we want to have our views call methods on our models that manipulate the state of our models. In response to the state of our models changing, our application does things. This could be updating some visual element, routing to a new hash fragment, and/or anything else we can do in a web page.

TODO: Build a simple todo app and replace the example code.

A simple app looks like this:

<html>
    <head>
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.7.0/underscore.js"></script>
        <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
        <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone.js"></script>
        <script>
            var MyView = Backbone.View.extend({
                id: 'title',
                initialize: function() {
                    this.$el.find('h1').css("color", "red");
                }
            });
            $(function() {
                var view = new MyView({'el': '#title'});
            });
        </script>
    </head>
    <body>
        <div id="title">
            <h1>Hello world!</h1>
        </div>
    </body>
</html>

2.1   Model

Backbone represents data as Models, which can be created, validated, destroyed, and saved to the server. For example, a simple model look like this:

app.Todo = Backbone.Model.extend({
    defaults: {
        title: '',
        completed: false
    },
});

Whenever a UI action causes an attribute of a model to change, the model triggers a "change" event; all the Views that display the model's state can be notified of the change so that they are able to respond accordingly.

Properties:

defaults
Used to specify the default attributes for your model. When creating an instance of the model, any unspecified attributes will be set to their default value.
get(attribute)

To access attributes, use get:

var todo = new app.Todo({title: "Get the milk"});
todo.get('title'); // "Get the milk"
todo.get("created_at"); // undefined
todo.set("created_at", Date());
todo.get("created_at"); // "Wed Sep 12 2012 12:51:17 GMT-0400 (EDT)"
parse(response, options)
Called whenever a model's data is returned by the server, in fetch, and save. The function is passed the raw response object, and should return the attributes hash to be set on the model. The default implementation is a no-op, simply passing through the JSON response. Override this if you need to work with a preexisting API, or better namespace your responses.
url()
The relative URL where the model's resource would be located on the server. [*] If your models are located somewhere else, override this method with the correct logic. Generates URLs of the form: "[collection.url]/[id]" by default, but you may override by specifying an explicit urlRoot if the model's collection shouldn't be taken into account.
urlRoot
Enables the default url() function to generate URLs based on the model id if you're using a model outside of a collection.

2.2   Collection

A collection is an ordered set of models.

A simple collection looks like this:

var Cars = Backbone.Collection.extend({
    model: app.Car,
});

2.3   View

A view is a subclass of Backbone.View that overrides the render() method:

var AppView = Backbone.View.extend({
    template: _.template($('#my-template').html()),

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

To construct a view, use the constructor as expected:

var appView = new AppView({
    el: el,
    model: model,
    foo: foo
});

Options passed to the view will be available on the this.options object. However, there are several special options that will be attached directly to the view if passed: model, collection, el, id, className, tagName, attributes, and events. If the view defines an initialize() function, it will be called when the view is first created. If you'd like to create a view that references an element already in the DOM, pass in the element as an option: new View({el: existingElement}).

Backbone will attach event listeners at instantiation time, right before invoking initialize.

Properties:

delegateEvents([events])
This function is called within the View's constructor for you, so you usually do not need to call this function yourself. However, if a parent view calls $this.$el.empty and re-renders the child view, you will need to invoke this since this.$el.empty removes event handlers.
$el
A cached jQuery object of the view's element.
el
All views have a DOM_ element at all time. If no element is passed to the constructor, Backbone will create a root element for the view automatically.
events

A hash or method which returns a hash from CSS selectors to either names of method on the view or standalone functions:

var MyView = Backbone.View.extend({
    events: {
        "dblclick": "open",
        "click .icon.doc": "select",
    },

    open: function() {
        window.open(this.model.get("viewer_url")),
    },

    select: function(e) {
        this.model.set({selected: true});
    }
})

Omitting the selector causes the events to be bound to the view's root element.

render
Render the view template from model data, and updates this.el with the new HTML. A good convention is to return this at the end of render to enable chained calls.
template
Backbone is agnostic with respect to your preferred method of HTML templating.

2.3.1   Rendering

Rendering views in Backbone can be tricky. To minimize complexity, it's useful to constrain what render() can do:

  1. render() should not cause side effects.
  2. render() should be idempotent, i.e. calling the effect of calling render() more than once should be identical to the effect of calling it once.
  3. render() should not destroy elements that it will construct again anyway.

To achieve the above with a view that has no subviews, you can use $.html():

render: function () {
    this.$el.html(this.template(options));
    return this;
}

However, html() will not work for views with subviews since it unbinds all jQuery events on all child nodes (via a call to empty()):

?

render() will work the first time you call, but if you call it again the events defined in subview.events will be unbound. [2] To fix this, a common solution is to have render() re-initialize subviews:

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

    this.subview1 = new Subview1();
    this.subview2 = new Subview2();

    this.$el.append(this.subview1.render().$el);
    this.$el.append(this.subview2.render().$el);

    return this;
}

But now render() re-initializes every descendant subview even though it only actually needs to rebind the DOM events. A better solution is to rebind events on your subviews whenever you call .html() via delegateEvents() . Since setElement() already calls delegateEvents(), a quick solution could look like this:

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

    this.subview1.setElement(this.$('.subview1')).render();
    this.subview2.setElement(this.$('.subview2')).render();
}

Or alternatively:

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

    this.assign(this.subview1, '.subview1');
    this.assign(this.subview2, '.subview2');
}

assign : function (view, selector) {
    view.setElement(this.$(selector)).render();
}

Less clear what to do for views with many children where we cannot attach directly to a selector:

var ParentView = Backbone.View.extend({
    initialize: function() {
        this.children = _.map(this.model.children, function(model) {
            return new ChildView({model: model});
        }, this);
    },
    render: function() {
        this.$el.html(this.template(this.model.toJSON()));
        _.each(this.children, function(child) {
            // This needs to call setElement
            child.render();
            this.$("#children").append(child.el);
        });
    },
    template: _.template($('#parent-template').html()),
});

Another downside is that if the parent needs to re-render, it will also rerender each of its children. Kind of needed though if we have this.$el.html() though... This will occur if a new item is added to a list of items. It's all or nothing. Actually, that's not true-- just use event handlers for "add" and "remove" events. That almost seems to imply render() is ever to be called once in the ideal scenario.


You should decompose your render() methods into smaller parts that let you render only what you need.

2.3.2   Patterns

2.3.2.1   ListView

A simple ListView:

var ListView = Backbone.View.extend({
    addOne: function(model) {
        var view = new this.options.viewClass({model: model});
        view.render();
        this.$el.append(view.el);
        this.views.push(view);
    },

    clear: function() {
        _.invoke(this.views, 'remove');
        this.views = [];
    },

    initialize: function() {
        this.views = [];
        this.listenTo(this.collection, 'add remove', this.render, this);
    },

    render: function() {
        this.clear();
        this.collection.each(this.addOne, this);
    }
});

3   Technical

3.1   Event aggregator

An event aggregator is a central object that manages the raising of events and the subscribers for those events. In terms of messaging patterns, the event aggregator is an in-memory, object based publish-subscribe model. It allows you have to have disparate parts of your system react to the events of other parts of the system, without having them directly coupled. [3]

A simple event aggregator might work like this [3]:

AddEditView = Backbone.View.extend({
  initialize: function(options){
    _.bindAll(this, "editMedication");
    options.vent.bind("editMedication", this.editMedication);
  },

  editMedication: function(medication){
    this.model = medication;
    this.render();
  }
});

MedicationView = Backbone.View.extend({
  events: {
    "click #edit": "editMedication"
  },

  initialize: function(options){
    this.vent = options.vent;
  },

  editMedication: function(){
    this.vent.trigger("editMedication", this.model);
  }
});

// Event aggregator object
var vent = _.extend({}, Backbone.Events);

var addEditView = new AddEditView({vent: vent});

medicationList.each(function(med){
  new MedicationView({model: med, vent: vent});
});

To make the event dispatcher global, add the event aggregator to the prototype of every core object:

var dispatcher = _.extend({}, Backbone.Events, {
  cid: "dispatcher"
});
_.each([
      Backbone.Collection.prototype,
      Backbone.Model.prototype,
      Backbone.View.prototype,
      Backbone.Router.prototype
  ], function(proto) {
  return _.extend(proto, {
    global_dispatcher: dispatcher
  });
});

3.2   Forms

It can be difficult to sync forms with models. A natural pattern might look this:

var Form = Backbone.View.extend({
    events: {
        'change input': 'set',
        'change select': 'set'
    },
    initialize: function() {
        this.listenTo(this.model, 'change', this.render);
    },
    render: function() {
        this.$el.html(this.template(this.model.attributes));
    },
    set: function(e) {
        self.model.set(e.target.name, e.target.value);
    },
    template: _.template($('template-form')).html()
});

However, this make it impossible for a user to tab through a form, since the form re-renders every time the user change an input.

A simple solution here would be to provide some option to interpolate the form without re-rendering the entire template:

var Form = Backbone.View.extend({
    events: {
        'change input': 'set',
        'change select': 'set'
    },
    interpolate: function(model) {
        _.each(model.changed, function(value, key) {
            this.$('input[name="' + key + '"]').val(value);
            this.$('select[name="' + key + '"]').val(value);
        }, this);
    },
    initialize: function() {
        this.listenTo(this.model, 'change', this.interpolate);
    },
    render: function() {
        this.$el.html(this.template(this.model.attributes));
        this.interpolate(this.model);
    },
    set: function(e) {
        self.model.set(e.target.name, e.target.value);
    },
    template: _.template($('template-form')).html()
});

3.3   View decomposition

The following example demonstrates that we sometime must decompose views in order to get the functionality we need. I haven't figured out the general principle yet though.

Consider a form element which has a submit button that should only be enabled if the form is valid:

<script type="text/template" id="form-template">
    <form>
        <input name="name" type="text" value="{{model.name}}">
        <button type="submit" {{ isValid ? "" : "disabled" }}>
    </form>
</script>
var Form = Backbone.View.extend({
    events: {
        'input input: set'
    },

    isValid: function() {
        return !_.some(this.$("[required]:visible"), function(field) {
            return $(field).val() === "";
        });
    },

    render: function() {
        this.$el.html(this.template(_.extend({
            isValid: this.isValid()
        }, this.model.attributes)));
    },

    set: function(e) {
        this.model.set(e.target.id, e.target.value, {silent: true});
    },

    template: _.template($('#form-template').html())
})

The above will not work very well. The trouble is that set() updates the model silently, which means the submit button will not become valid when the user fills in the name. However, if we disable silent, then every time the user inputs a character into the input, the view will be rendered and the user will not be able to type.

To fix this, we can decompose the view into two parts and reassign isValid to the model:

<script type="text/template" id="form-template">
    <form>
        <input name="name" type="text" value="{{model.name}}">
        <div class="controls"></div>
    </form>
</script>

<script type="text/template" id="button-submit-template">
    <button type="submit" {{ isValid ? "" : "disabled" }}>
</script>
var ButtonSubmit = Backbone.View.extend({
    initialize: function() {
        this.listenTo(this.model, 'change', this.render);
    }

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

    template: _.template($('#button-submit-template').html())
});

var Form = Backbone.View.extend({
    events: {
        'input input: set'
    },

    initialize: function() {
        this.buttonSubmit = new ButtonSubmit({model: model});
    },

    render: function() {
        this.$el.html(this.template(_.extend(this.model.attributes)));
        this.buttonSubmit.setElement(this.$('.controls')).render();
    },

    set: function(e) {
        this.model.set(e.target.id, e.target.value, {silent: true});
    },

    template: _.template($('#form-template').html())
})

4   Market

Airbnb, Foursquare, and Stripe use Backbone.js. Stripe is moving to Ember though.

As of Jun 17, 2014:

Stars:18310
Forks:7077
Commits:2616
Contributors:226
SO questions:20705

5   History

Jeremy Ashkenas is a Graphics Editor at the New York Times. Released to the public in 2011.

Ashkensas extracted Backbone from DocumentCloud, the original Backbone application. The Night Foundation, left a large part of their fortune which give out grants to journalism/technology projects. The News Challenge is a contest where they give away money to open source projects that aim at helping journalists get their job done or readers read the news. DocumentCloud was one of the larger winners in 2009. Most of the source materials for journalists is from documents. Typically source material is archived and forgotten. But online, you have as much space as you want, and crazy that you wouldn't want to share sources with your story. [4]

The Night Foundation. Everything you do as part of the New challenge must be open-sourced. Typically people just publish a tarball after they finish the application. They wanted to examine afterward. UnderscoreJS came out of that which served as the core of Backbone and can be used on its own right. Also Jammit, which is an `asset pipeline`_. [4]

6   FAQ

6.1   How can I use Mustache template syntax?

To make Backbone use Mustache template syntax you can override the templateSetting variable of Underscore.js:

_.templateSettings = {
    evaluate: /\[\[(.+?)\]\]/g,
    interpolate: /\{\{(.+?)\}\}/g
};

Then you can use {{  }} instead of <%=  %> and [[ ]] instead of <% %>.

6.2   How do I fetch relationships of a model?

Consider the following:

var Parent = Backbone.Model.extend({
    parse: function(response) {
        response.children = _.map(response.children, function(id) {
            var child = Child({id: id});
            child.fetch(); // This is asynchronous, so it fails
            return child.attributes;
        });
        return response;
    },
    url: "/parents"
});

Possible solutions....

  • Extend Backbone.Model to have a synchronous fetch
  • Delay parsing children until Parent.initialize(), but this suffer the same problem since initialize() is synchronous. This is also bad, because the Parent is in a sort of inconsistent state.
  • Delay parsing until render(), but that mixes up initialization logic with render().

7   Further reading

8   Footnotes

[*]This violates HATEOAS.

9   References

[1]http://addyosmani.github.io/backbone-fundamentals/
[2]Ian Storm Taylor. July 2012. Rendering Views in Backbone.js Isn't Always Simple. http://ianstormtaylor.com/rendering-views-in-backbonejs-isnt-always-simple/
[3](1, 2) Derick Bailey. July 19, 2011. References, Routing, And The Event Aggregator: Coordinating Views In Backbone.js http://lostechies.com/derickbailey/2011/07/19/references-routing-and-the-event-aggregator-coordinating-views-in-backbone-js/
[4](1, 2, 3, 4, 5, 6) Jeremy Ashkenas. May 26, 2013. Taking JavaScript Seriously with Backbone.js. https://www.youtube.com/watch?v=4udR30JYenA

Children should not maintain references to parent views. Instead, either change a model that the parent is observing, or trigger an event on the child view itself or on another backbone "Events"-based object.


Not sure how to deal with templates that may omit elements from them. The solution generally must be to rerender. I would prefer not append elements within the view but re-rendering is not always an option.


Calling view.remove() not only removes the view, its nested views, etc. but also the container element ($el). Because of this, you cannot create a view, remove it and create another view in the same container element.

You can use view.$el.empty() to remove everything except the container. However, you should not override remove to do this, since that would change the semantics. Instead I would create a new method empty. However, I'm not sure that's even what we want.

Jeremey Ashkenas cryptically writes in response to "the container DOM element was part of the web page before inserting/injecting a View into it. Why should it be removed if I just remove the View?":

It can be, but if you're doing your views "right", then it certainly isn't. ;)

The only solution I can think of it use a template, and then to rerender. Therefore, even if the element is removed its just recreated anyway.

https://github.com/jashkenas/backbone/issues/2834


http://geeks.bizzabo.com/post/83917692143/7-battle-tested-backbonejs-rules-for-amazing-web-apps

When a view is removed from the DOM using the remove method, it must unbind from all the events it is bound to.

If you've used on for binding, it’s your duty to use off for unbinding. Without unbinding, the garbage collector can’t release the memory for you and your apps performance will degrade.

That's where listenTo comes in. It tracks what the view is bound to and unbinds it on a remove. Backbone does this by calling stopListening just before removing itself from the DOM:

// Ok:
this.stateModel.on('change:readMore', this.renderReadMore, this);

// Awesome:
this.listenTo(this.stateModel, 'change:readMore', this.renderReadMore);

Principles:

  1. DOM Events only affect models
  2. DOM Changes only when the model changes
  3. Events are better than callbacks (not clear why)

Data belongs to models not views. Next time you find yourself storing data inside a view (or worse: the DOM), stop and move it into the model.

If you don’t have a model, create one, it’s easy:

this.state = new Backbone.Model();