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.