Backbone Notes

Models

Create a model class:

var Appointment = Backbone.Model.extend({});  

You can add defaults to a class:

var Appointment = Backbone.Model.extend({  
  defaults: { title: 'Checkup', date: new Date() }
});

Or as a function to defer creation for new objects until they are newed

var Appointment = Backbone.Model.extend({  
  defaults: function() {
    return {
      title: 'Checkup',
      date: new Date()
    }
  }
});

Create an instance of our model

var appointment = new Appointment({title: "Dinner with Adam"})  

Get properties on an instance

appointment.get('title');  

Set properties on an instance

appointment.set('title', 'My Backbone Hurts');  

To convert an object to JSON use the toJSON function

var appointment = new Appointment({id: 1});  
console.log(appointment.toJSON());  

Syncing Data

You can set the default root url for you model

var Appointment = Backbone.Model.extend({urlRoot: '/appointments'});  

You can then fetch items using the default which uses RESTful routing via Rails style routing ie: /appointments/{id}

var appointment = new Appointment({id:1})  
appointment.fetch();  

Or you can override the routing for a specific instance

var appointment = new Appointment({id:1})  
appointment.url = "/appointments/details/1";  
appointment.fetch();  

When calling fetch, you can cause a collection to reset by including the reset as an option.

appointments.fetch({reset: true});  

The attributes property on an instance represents the underlaying POJO data for the model

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

You can override the parsing when fetching by overriding the parse method. The parse method can be extended to match however your data is passed back:

Actual:

{ "appointment": { "title": "Ms. Kitty Hairball Treatment", "cankelled": false, "identifier": 1 }

Default:

{ "title": "Ms. Kitty Hairball Treatment", "cancelled": false, "id": 1 }
var Appointment = Backbone.Model.extend({  
  parse: function(response){
    response = response.appointment;
    response.cancelled = response.cankelled;
    delete response.cankelled;
    return response;
  }
});

You also use the idAttribute property for custom identifiers. Usually this maps to id.

var Appointment = Backbone.Model.extend({  
  parse: function(response){
    var appointment = response.appointment;
    appointment.cancelled = appointment.cankelled;
    delete appointment.cankelled;
    return appointment;
  },
  idAttribute: "identifier"
});

Additionally, when an object is constructed, you can use pass {parse:true} to the constructor to automatically parse the objects passed to the constructor:

var data = { "appointment": { "title": "Ms. Kitty Hairball Treatment", "cankelled": false, "identifier": 1 }  
var appointment = new Appointment(data, {parse: true});  

You can persist changes

appointment.set({'cancelled',true});  
appointment.save();  

You can customize persistence as well by overriding the toJSON method in the model. This allows you to convert back to the JSON format submitted. toJSON should only be used for serializing the data back to the server. Use the attributes property for rendering the views.

var Appointment = Backbone.Model.extend({  
  toJSON: function(){
    var result = _.clone(this.attributes);
    result.cankelled = result.cancelled;
    delete result.cancelled;
    return { "appointment" : result }
  }
});

Overriding persistence to only make an object readonly. You modify the sync method on a Model:

App.Models.Appointment = Backbone.Model.extend({  
  sync: function(method, model, options){
    switch(method) {
      case "read":
      case "create":
        Backbone.sync(method, model, options);
        break;
    };
  }
});

Events

You can add event handlers to models:

appointment.on('my-event', myEventHandler);  

You can manually fire events

appointment.trigger('my-event');  

Change event listens for any properties getting set via set. You can override this by passing {silent:true} to the set call

appointment.set({title:'This is a silent change'}, {silent:true});  

You can remove an event with

appointment.off('plerp');  

Special Events
change - When an attribute is modified
destroy - When a model is destroyed
sync - Whenever successfully synced
error - When model save or validation fails
all - Any triggered event

You can pass optional parmaeters to events as well:

var AppointmentView = Backbone.View.extend({  
  template: _.template(""),
  initialize: function(){
    this.model.on('change:title', this.changedTitle, this);
  },
  render: function(){
    this.$el.html(this.template(this.model.attributes));
  },
  changedTitle: function(model, value, options){
    this.$('span').html(value);
    if(options.highlight) {
      this.$el.effect('highlight', {}, 1000);
    }
  }
});

var appointment = new Appointment({title: "Toothache"});  
var appView = new AppointmentView({model: appointment});  
appointment.set({title: "General Cleaning"}, {highlight: false});  

In new Backbone, you should have views listen to their model with the listenTo method instead of this.model.on. ListenTo will correctly stop listening to the model when a view is removed. This works by calling stopListening when remove is called on a View. More info is here
http://stackoverflow.com/questions/14041042/backbone-0-9-9-difference-between-listento-and-on

Syntax is different for Listen To:

this.listenTo(this.model, 'change', this.render, this);  
this.model.on('change', this.render, this);  

Full example:

var AppointmentView = Backbone.View.extend({  
  template: _.template(""),
  initialize: function(){
    this.listenTo(this.model, 'change:title', this.changedTitle, this);
  },
  render: function(){
    this.$el.html(this.template(this.model.attributes));
  },
  changedTitle: function(model, value, options){
    this.$('span').html(value);
    if (options.highlight !== false){
      this.$el.effect('highlight', {}, 1000);
    }
  }
});

Views

Create a blank view class

var AppointmentView = Backbone.View.extend({});  

Create a new instance of our view class and pass in the model

var appointmentView = new AppointmentView({model:appointment});  

Override the render method to allow rendering. Inside the view, this.el allows access to the top level element for the view. Additionally, you can access the model that the view is assigned to.

var AppointmentView = Backbone.View.extend({  
  render: function() {
    var html = '<ul><li>' + this.model.get('title') + '</li></ul>';
    $(this.el).append(html);
  }
});

Finally, you can render the view after you have created an instance and attached a model

appointmentView.render();  
$('#app').html(appointmentView.el);

Changing default properties of a view such as default tag or the class:

var AppointmentView = Backbone.View.extend({  
  tagName: 'li',
  className: 'appointment'
});

Templating

You can add a templating function, the default uses underscores, to render stuff

var AppointmentView = Backbone.View.extend({  
  template: _.template(''),
  ...
});

To make the render function use the template, first convert the model to attributes, then pass the JSON to the template function

var AppointmentView = Backbone.View.extend({  
  template: _.template(''),
  render: function(){
    var attributes = this.model.toJSON();
    this.$el.html(this.template(attributes));
  }
});

You can handle UI events inside view by attaching events. Events take the form "event":"functionName"

var AppointmentView = Backbone.View.extend({  
  template: _.template(''),
  events: {
    "click": function() { alert(this.model.get('title'));}
  },
  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  }
});

You can make a view render into an existing element by setting the el property in the constructor

var appointmentsView = new AppointmentsView({collection: appointments, el: $('#app')});  

The initialize method for a view takes additional parameters added to the constructor object of the view. This allows additional options to be passed to the view on creation:

var AppointmentsView = Backbone.View.extend({  
  initialize: function(options) {
    this.doctor = options.doctor;
  }
});
var drGoodparts = new Doctor({name: "Dr. Goodparts"});  
var appView = new AppointmentsView({doctor: drGoodparts});  

You can also escape content when rendering a view by passing the model to the template and using the escape method on the model

var AppointmentView = Backbone.View.extend({  
  template: _.template(""),
  render: function(){
    this.$el.html(this.template({model: this.model}));
  }
});

Events Between Models and Views

When events from the view need to modify the model, the model should be responsible for changing its own state. This way the view is not making state changes, nor is it calling data sync function.

The view will call the model's methods to accomplish the state change. In this example, when the link event is firedon the view, the view calls the models method to change its state:

var AppointmentView = Backbone.View.extend({  
  template: _.template(' <a href="#">x</a>'),
  events: { "click a": "cancel" },
  cancel: function(){
    this.model.cancel();
  },
  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  }
});

var Appointment = Backbone.Model.extend({  
  cancel: function() {
    this.set({"cancelled": true});
  }
});

Finally, changes of state should be re-rended by making the view LISTEN for state changes on the model inside of its initialize function. The view SHOULD NOT call render manually.

var AppointmentView = Backbone.View.extend({  
  template: _.template('<a href="#">x</a>'),
  initialize: function() {
    this.model.on('change', this.render, this);
  },
  events: { "click a": "cancel" },
  cancel: function(){
    this.model.cancel();
    this.render();
  },
  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  }
});

Lastly, you can listen for destroy events and have the view remove itself when the model is destroyed:

var AppointmentView = Backbone.View.extend({  
  template: _.template('<a href="#">x</a>'),
  initialize: function(){
    this.model.on('change', this.render, this);
    this.model.on('destroy', this.remove, this);
  },
  remove: function() {
    this.$el.remove();
  },
  events: { "click a": "cancel" },
  cancel: function(){
    this.model.cancel();
  },
  render: function(){
    this.$el.html(this.template(this.model.toJSON()));
  }
});

Forms

Forms are usually created as a separate View.

var AppointmentForm = Backbone.View.extend({  
  template: _.template('<input name="title" type="text" value="" /><input name="name" type="text" value="" />'),
  render: function() {
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});

You then bind the submit event to retrieve the form contents and pass it the save method on the model. By passing values to Save, I assume it automatically calls set for each property passed to the method. Additionally, you can use a callback to watch for success or failure of the save and perform proper redirection.

var AppointmentForm = Backbone.View.extend({  
  template: _.template('<input name="title" type="text" value="" /><input name="name" type="text" value="" />'),
  render: function(){
    this.$el.html(this.template(this.model.attributes));
    return this;
  },
  events: {
    submit: "save"
  },
  save: function(e){
    e.preventDefault();
    var newTitle = this.$('input[name=title]').val();
    var newName = this.$('input[name=name]').val();
    this.model.save({title: newTitle, name: newName}, {
      success: function(){
        Backbone.history.navigate('', {trigger: true});
      },
      error: function(model, xhr, options) {
        var errors = JSON.parse(xhr.responseText).errors;
        alert(errors);
      }
    });
  }
});

App Organization with App View

We use an app view to trap default events (such as clicks) and create a start method that we can call to bootstrap the entire app!

var AppointmentApp = new (Backbone.View.extend({  
  Collections: {},
  Models: {},
  Views: {},
  events: {
    'click a[data-backbone]': function(e){
      e.preventDefault();
      Backbone.history.navigate(e.target.pathname, { trigger: true });
    }
  },
  start: function(bootstrap){
    this.appointments = bootstrap.appointments
    var appointmentsView = new AppointmentApp.Views.Appointments({collection: this.appointments});
    $('#app').html(appointmentsView.render().el);
  }
}))({el: document.body});

Mustache Templating

Use Mustache.compile to compile a template. Variables are put between curly braces

var AppointmentForm = Backbone.View.extend({  
  template: Mustache.compile('<input type="text" name="name" />')
  render: function(){
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});

You can render collections quite easily by iterating over the name of the collection. You don't even have to references the name of the iterated object, just the property you want to render! In this example, since we are iterating over an Array of dates, you simply use {{.}} to reference the current iterated object.

App.Views.Appointment = Backbone.View.extend({  
  template: Mustache.compile('<h2>{{ title }}</h2> Possible Dates: <ul>{{ #possibleDates}}<li>{{.}}</li></ul>{{/possibleDates}}'),
  render: function(){
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});

Or if the array contained objects with "day" and "time" properties

App.Views.Appointment = Backbone.View.extend({  
  template: Mustache.compile('<h2>{{ title }}</h2> Possible Dates: <ul>{{ #possibleDates }}<li>{{day}} at {{time }}</li></ul>{{/possibleDates}}'),
  render: function(){
    this.$el.html(this.template(this.model.attributes));
    return this;
  }
});

Collections

Create a new list based on a Model

var AppointmentList = Backbone.Collection.extend({model:Appointment});  

You can reset the list by applying JSON directly to it:

var appointments = new AppointmentList();  
var json = [  
  {title: 'Back pain'},
  {title: 'Dry mouth'},
  {title: 'Headache'}
];
appointments.reset(json);  

You can set the source url property an use fetch to retrieve the objects

var AppointmentList = Backbone.Collection.extend({  
  model: Appointment,
  url: '/appointments'
});
var appointments = new AppointmentList();  
appointments.fetch();  

You can listen for events on the collection. These events are:
add, remove, reset

var appointments = new AppointmentList();  
appointments.on('reset', function() { alert(this.length); })  
appointments.fetch();  

There are a number of metadata functions you can use to work with the collection. These are part of the underscore library and can be viewed in the documentation.

Collection Views

Collection views contain model/view pairs. Collection views don't render their own HTML, they delegate that responsibility to the model views as shown in these examples.

Instead of a mode property, they have a collection property that gets set to a Collection instance.

var appointments = new AppointmentList();  
var AppointmentListView = Backbone.View.extend({});  
var appointmentListview = new AppointmentListView({collection: appointments});  

When rendering a collection, we iterate over the collection and render each element using it's own view.

var AppointmentListView = Backbone.View.extend({  
  render: function(){
    this.collection.forEach(this.addOne, this);
  },
  addOne: function(model){
    var view = new AppointmentView({model: model});
    view.render();
    this.$el.append(view.el);
  }
});

You can bind list events to the view in initialize function:

var AppointmentListView = Backbone.View.extend({  
  initialize: function(){
    this.collection.on('add', this.addOne, this);
    this.collection.on('reset', this.render, this);
  },
  render: function(){
    this.collection.forEach(this.addOne, this);
  },
  addOne: function(model){
    var appointmentView = new AppointmentView({model: model});
    appointmentView.render();
    this.$el.append(appointmentView.el);
  }
});

When fetching data from the server. You can add properties to the collection for things such as paging. You can override the parse function to do this

// Get /appointments
{
  "per_page": 10, "page": 1, "total": 50,
  "appointments": [
    { "title": "Ms. Kitty Hairball Treatment", "cankelled": false, "identifier": 1 }
  ]
}
var Appointments = Backbone.Collection.extend({  
  parse: function(response){
    this.per_page = response.per_page;
    this.page = response.page;
    this.total = response.total;
    return response.appointments;
  } 
});

You can call fetch with specific parameters by passing in a data object.

var appointments = new Appointments();  
appointments.fetch({data: {since: '2013-01-01'}});  

You can sort collections by setting the comparator property

var Appointments = Backbone.Collection.extend({  
  comparator: "date"
});

You can also make this a function

var Appointments = Backbone.Collection.extend({  
  comparator: function(appt1, appt2) {
    return appt1.get('date') &lt; appt2.get('date');
  }
});

You can also search for values using where

var Appointments = Backbone.Collection.extend({  
  cancelledCount : function() {
    return this.where({ cancelled: true }).length;
  }
});

Routers

Create a router

var AppRouter = Backbone.Router.extend({});  

Create routes in the router by adding them to the router's route property.

var AppRouter = Backbone.Router.extend({  
  routes: {
    "appointments/:id" : "show"
  },
  show: function(id){
    console.log("heyo we're in show with id %d", id);
  }
});

Enable push state with the history

Backbone.history.start({pushState:true})  

When calling a router, you can render the view to the app:

var AppRouter = Backbone.Router.extend({  
  routes: { "appointments/:id": "show" },
  show: function(id){
    var appointment = new Appointment({id: id});
    appointment.fetch();
    var appointmentView = new AppointmentView({model: appointment});
    appointmentView.render();
    $("#app").html(appointmentView.el);
  }
});

Putting it all together:

var AppRouter = new (Backbone.Router.extend({  
  routes: { "appointments/:id": "show", "": "index" },

  initialize: function(options){
    this.appointmentList = new AppointmentList();
  },

  start: function() {
    Backbone.history.start({pushState:true});
  },

  index: function(){
    var appointmentsView = new AppointmentListView({collection: this.appointmentList});
    appointmentsView.render();
    $('#app').html(appointmentsView.el);
    this.appointmentList.fetch();
  },

  show: function(id){
    var appointment = new Appointment({id: id});
    var appointmentView = new AppointmentView({model: appointment});
    appointmentView.render();
    $('#app').html(appointmentView.el);
    appointment.fetch();
  }
}))();

$(function(){
  AppRouter.start();
})

You can add optional parts to a route by wrapping parameters in parenthesis. This allows you to overload a route function that matches multiple route definitions

var AppRouter = new (Backbone.Router.extend({  
  routes: {
    "appointments/p:page(/pp:per_page)": "page"
  },
  page: function(page, per_page){
    per_page = per_page || 10;
    this.appointments.fetch({data: {page: page, per_page: per_page}});
  }
}));

You can also add a trailing slash to the end of a route by adding an optional (/) to the route definition

var AppRouter = new (Backbone.Router.extend({  
  routes: {
    "appointments/p:page(/pp:per_page)(/)": "page"
  },
  page: function(page, per_page){
    per_page = per_page || 10; 
    this.appointments.fetch({data: {page: page, per_page: per_page}});
  }
}));

You can use the decodeURIComponent function to decode parameters passed to a route function

var AppRouter = new (Backbone.Router.extend({  
  routes: {
    "appointments/p:page(/pp:per_page)(/)": "page"
  },
  page: function(page, per_page){
    this.appointments.fetch({data: {page: decodeURIComponent(page), per_page: decodeURIComponent(per_page)}});
  }
}));

You can use Regular Expressions to force parameters to match your URL by removing them from the routes object and manually adding the route to via the initialize method:

var AppRouter = new (Backbone.Router.extend({  
  routes: {
  },
  initialize: function() {
    this.route(/^appointments\/(\d+)$/, 'show');
  },
  show: function(id){
    var appointment = new Appointment({id: id});
    console.log(appointment);
  }
}));

You can add a catch all route with *path

var AppRouter = new (Backbone.Router.extend({  
  routes: {
    "appointments/:id": "show",
    "*path": "notFound"
  },
  show: function(id){
    var appointment = new Appointment({id: id});
    console.log(appointment);
  },
  notFound: function() {
    console.log('Route does not exist.');
  }
}));

Learn Backbone.js Completely Link

Simple example (Wine Cellar)

Responsibilities of Parts of Backbone

a router should never instantiate a view and manipulate the DOM directly with jQuery or placing the view in to the DOM. That’s the responsibility of the application and it’s state

Three Stages of Backbone Development

File and Folder Structure

comments powered by Disqus