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') < 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.');
}
}));
Links
Learn Backbone.js Completely Link
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