Backbone.js sample app
For writing a single-page web app, you want something more structured than jQuery. You want a framework that gives you a model-view-something pattern to organize your code around. Google has two: GWT and Closure. Cappucino and Sproutcore are two other choices. Most of these are meant to be used instead of jQuery. Three others, Knockout.js (made by a Microsoft developer), Javascript MVC and Backbone.js (neither connected with Microsoft), are meant to be used in addition to jQuery.
Here is a sample application that uses most of Backbone.js’s features.
First, there’s the HTML page:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Backbone.js SPI</title> </head> <body> <ul> <li><a id="os-link" href="#/os/">Operating System</a></li> <li><a id="cpu-link" href="#/cpu/">Central Processing Unit</a></li> </ul> <div id="editor"></div> <div id="data"></div> <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script> <script src="underscore-min.js"></script> <script src="backbone-min.js"></script> <script src="soyutils.js"></script> <script src="cpu_templates.js"></script> <script src="os_templates.js"></script> <script src="backbone-spi.js"></script> </body> </html>
And then we have two Closure templates:
{namespace SPI.CPU.Templates}
/**
* The CPU editing field.
* @param value The field's value.
*/
{template .editor}
<label for="cpu-input">Central Processing Unit:</label>
<input type="text" id="cpu-input" value="{$value}">
{/template}
/**
* The CPU data display field.
* @param value The field's value.
*/
{template .data}
<p>
CPU: {$value}
</p>
{/template}
{namespace SPI.OS.Templates}
/**
* A new operating system. To be added.
*/
{template .newOS}
<p>
<input id="os-input" placeholder="Operating System" type="text">
<button>Add</button>
</p>
{/template}
/**
* An OS in the list.
* @param name The name of the OS.
* @param count The number of installations.
*/
{template .os}
<td><input type="number" value="{$count}"></td>
<td><button>Remove</button></td>
<td>{$name}</td>
{/template}
/**
* The table of operating systems.
*/
{template .table}
<p>
Number of operating systems: <span id="os-count"></span>
</p>
<table>
<thead>
<tr>
<th></th>
<th></th>
<th>Operating System</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
{/template}
And this is the Javascript file. It’s maintainable and it works:
/*global _, $, Backbone */
var SPI = SPI || {};
SPI.CPU = SPI.CPU || {};
SPI.CPU.Models = SPI.CPU.Models || {};
SPI.CPU.Models.CPU = Backbone.Model.extend({
initialize: function () {
this.set({'cpu': ''});
}
});
SPI.CPU.Views = {};
SPI.CPU.Views.Editor = Backbone.View.extend({
cpuInput: function () {
var href = $('#cpu-link').attr('href'),
val = $('#cpu-input').val();
if (val.length > 0) {
href = '#/cpu/' + val + '/';
$('#cpu-link').attr('href', href);
Backbone.history.saveLocation('/cpu/' + val + '/');
} else {
$('#cpu-link').attr('href', '#/cpu/');
Backbone.history.saveLocation('/cpu/');
}
this.model.set({'cpu': $('#cpu-input').val()});
},
el: $('#editor')[0],
events: {
'keyup input': 'cpuInput'
},
render: function () {
$(this.el).html(SPI.CPU.Templates.editor({
value: this.model.get('cpu')
}));
this.delegateEvents();
}
});
SPI.CPU.Views.Data = Backbone.View.extend({
initialize: function () {
_(this).bindAll('render');
this.model.bind('change', this.render);
},
el: $('#data')[0],
render: function () {
$(this.el).html(SPI.CPU.Templates.data({
value: this.model.get('cpu')
}));
}
});
SPI.OS = SPI.OS || {};
SPI.OS.Model = Backbone.Model.extend({
initialize: function () {
this.set({
'count': 0
});
}
});
SPI.OS.Collection = Backbone.Collection.extend({
model: SPI.OS.Model
});
SPI.OS.Views = SPI.OS.Views || {};
SPI.OS.Views.NewOS = Backbone.View.extend({
el: $('#editor')[0],
events: {
'click button': 'addOS'
},
initialize: function () {
_(this).bindAll('render');
},
addOS: function () {
var val = $.trim($('input').val());
if (val.length > 0) {
this.collection.add(new SPI.OS.Model({
count: 0,
name: $('input').val()
}));
}
},
render: function () {
$(this.el).html(SPI.OS.Templates.newOS());
this.delegateEvents();
}
});
SPI.OS.Views.OS = Backbone.View.extend({
change: function () {
var val = parseInt($(this.el).find('input').val(), 10);
this.model.set({
'count': val
});
},
events: {
'change input': 'change',
'click button': 'remove'
},
initialize: function () {
_(this).bindAll('change', 'remove', 'render');
},
remove: function () {
this.collection.remove(this.model);
},
render: function () {
$(this.el).html(SPI.OS.Templates.os({
count: this.model.get('count'),
name: this.model.get('name')
}));
this.delegateEvents();
}
});
SPI.OS.Views.List = Backbone.View.extend({
el: $('#data')[0],
initialize: function () {
_(this).bindAll('render');
},
render: function () {
$(this.el).html(SPI.OS.Templates.table());
}
});
SPI.OS.Views.TBody = Backbone.View.extend({
add: function (model) {
var view = new SPI.OS.Views.OS({
el: $('<tr>').appendTo(this.el)[0],
model: model,
collection: this.collection
});
this.views.push(view);
view.render();
},
initialize: function () {
_(this).bindAll('add', 'remove', 'render');
this.views = [];
this.collection.bind('add', this.add);
this.collection.bind('remove', this.remove);
},
remove: function (model) {
var i;
for (i = 0; i < this.views.length; i += 1) {
if (this.views[i].model === model) {
$(this.views[i].el).remove();
this.views.splice(i, 1);
break;
}
}
},
render: function () {
var that = this;
_(this.views).each(function (view) {
view.el = $('<tr>').appendTo(that.el)[0];
view.render();
});
}
});
SPI.OS.Views.Count = Backbone.View.extend({
initialize: function () {
_(this).bindAll('render');
this.collection.bind('change', this.render);
this.collection.bind('remove', this.render);
},
render: function () {
var count = 0;
this.collection.each(function (os) {
count += os.get('count');
});
$(this.el).html(count);
}
});
SPI.Controller = Backbone.Controller.extend({
initialize: function () {
var cpu = {},
os = {},
osCollection = new SPI.OS.Collection();
cpu.model = new SPI.CPU.Models.CPU();
cpu.views = {};
cpu.views.editor = new SPI.CPU.Views.Editor({
model: cpu.model
});
cpu.views.data = new SPI.CPU.Views.Data({
model: cpu.model
});
os.views = {};
os.views.newOS = new SPI.OS.Views.NewOS({
collection: osCollection
});
os.views.list = new SPI.OS.Views.List({
collection: osCollection
});
os.views.count = new SPI.OS.Views.Count({
collection: osCollection
});
os.views.tbody = new SPI.OS.Views.TBody({
collection: osCollection
});
this.route(/\/cpu\/(?:(\w+)\/)?$/, 'cpu', function (value) {
if (value === undefined) {
value = '';
}
cpu.model.set({
'cpu': value
}, {
silent: true
});
cpu.views.editor.render();
cpu.views.data.render();
});
this.route(/\/os\/$/, 'os', function () {
os.views.newOS.render();
os.views.list.render();
os.views.count.el = $('#os-count')[0];
os.views.count.render();
os.views.tbody.el = $('table > tbody')[0];
os.views.tbody.render();
});
}
});
$(function () {
window.controller = new SPI.Controller();
Backbone.history.start();
window.location.hash = '#/cpu/';
});
Expect a future update to feature the same app written with Knockout.js and Sammy.js.