Who Uses Backbone?
airbnb, newsblur, disqus, hulu, basecamp, stripe, irccloud, trello, …
Why?
http://backbonejs.org/#FAQ-why-backbone
It is not a framework
Backbone is an MVP (Model View Presenter) javascript library that, unlike Django, is extremely light in its use of conventions. Frameworks are commonly seen as fully-working applications that run your code, as opposed to libraries, where you import their code and run it yourself. Backbone falls solidly into the latter category and it’s only through the use of the Router class that it starts to take some control back. Also included are View, Model and Collection (of Models), and Events, all of which can be used as completely standalone components and often are used this way alongside other frameworks. This means that if you use backbone, you will have much more flexibility for creating something unusual and being master of your project’s destiny, but on the other hand you’ll be faced with writing a lot of the glue code yourself, as well as forming many of the conventions.
Dependencies
Backbone is built upon jQuery and underscore. While these two libraries have some overlap, they mostly perform separate functions; jQuery is a DOM manipulation tool that handles the abstractions of various browser incompatibilities (and even in the evergreen browser age offers a lot of benefits there), and underscore is primarily a functional programming tool, offering cross-browser support for map, reduce, and the like. Most data manipulation you do can be significantly streamlined using underscore and, in the process, you’ll likely produce more readable code. If you’re on a project that isn’t transpiling from ES6 with many of the functional tools built in, I enthusiastically recommend using underscore.
Underscore has one other superb feature: templates.
// Using raw text var titleTemplate = _.template('<h1>Welcome, <%- fullName %></h1>'); // or if you have a <script type="text/template"> var titleTemplate = _.template($('#titleTemplate')); // or if you're using requirejs var titleTemplate = require('tpl!templates/title'); var renderedHtml = titleTemplate({title: 'Martin Sheen'}); $('#main').html(renderedHtml);
Regardless of how you feel about the syntax (which can be changed to mustache-style), having lightweight templates available that support escaping is a huge win and, on its own, enough reason to use underscore.
Backbone.Events
Probably the most reusable class in Backbone’s toolbox is Events. This can be mixed in to any existing class as follows:
// Define the constructor var MyClass = function(){}; // Add some functionality to the constructor's prototype _.extend(MyClass.prototype, { someMethod: function() { var somethingUseful = doSomeThing(); // trigger an event named 'someEventName' this.trigger('someEventName', somethingUseful); } }); // Mix-in the events functionality _.extend(MyClass.prototype, Backbone.Events);
And suddenly, your class has grown an event bus.
var thing = new MyClass(); thing.on('someEventName', function(somethingUseful) { alert('IT IS DONE' + somethingUseful); }); thing.someMethod();
Things we don’t know about yet can now listen to this class for events and run callbacks where required.
By default, the Model, Collection, Router, and View classes all have the Events functionality mixed in. This means that in a view (in initialize
or render
) you can do:
this.listenTo(this.model, 'change', this.render);
There’s a list of all events triggered by these components in the docs. When the listener also has Events mixed in, it can use .listenTo
which, unlike .on
, sets the value of this
in the callback to the object that is listening rather than the object that fired the event.
Backbone.View
Backbone.View is probably the most useful, reusable class in the Backbone toolbox. It is almost all convention and does nothing beyond what most people would come up with after pulling together something of their own.
Fundamentally, every view binds to a DOM element and listens to events from that DOM element and any of its descendants. This means that functionality relating to your application’s UI can be associated with a particular part of the DOM.
Views have a nice declarative format, as follows (extend is an short, optional shim for simplifying inheritance):
var ProfileView = Backbone.View.extend({ // The first element matched by the selector becomes view.el // view.$el is a shorthand for $(view.el) // view.$('.selector') is shorthand for $(view.el).find('.selector'); el: '.profile', // Pure convention, not required template: profileTemplate, // When events on left occur, methods on right are called events: { 'click .edit': 'editSection', 'click .profile': 'zoomProfile' }, // Custom initialize, doesn't need to call super initialize: function(options) { this.user = options.user; }, // Your custom methods showInputForSection: function(section) { ... }, editSection: function(ev) { // because this is bound to the view, jQuery's this is made // available as 'currentTarget' var section = $(ev.currentTarget).attr('data-section'); this.showInputForSection(section); }, zoomProfile: function() { $(ev.currentTarget).toggleClass('zoomed'); }, // Every view has a render method that should return the view render: function() { var rendered = this.template({user: this.user}); this.$el.html(rendered); return this; } });
Finally, to use this view:
// You can also pass model, collection, el, id, className, tagName, attributes and events to override the declarative defaults var view = new ProfileView({ user: someUserObject }); view.render(); // Stuff appears in .profile ! // Once you've finished with the view view.undelegateEvents(); view.remove(); // NB. Why doesn't remove call undelegateEvents? NFI m8. Hate that it doesn't.
The next step is to nest views. You could have a view that renders a list but for each of the list items, it instantiates a new view to render the list item and listen to events for that item.
render: { // Build a basic template "<ul></ul>" this.$el.html(this.template()); _.each(this.collection.models, function(model) { // Instantiate a new "li" element as a view for each model var itemView = new ModelView({ tagName: 'li' }); // Render the view itemView.render(); // The jquery-wrapped li element is now available at itemView.$el this.$('ul') // find the ul tag in the parent view .append(itemView.$el); // append to it this li tag }); }
How you choose to break up your page is completely up to you and your application.
Backbone.Model and Backbone.Collection
Backbone’s Model and Collection classes are designed for very standard REST endpoints. It can be painful to coerce them into supporting anything else, though it is achievable.
Assuming you have an HTTP endpoint /items, which can:
- have a list of items GET’d from it
- have a single item GET’d from /items/ID
And if you’re going to be persisting any changes back to the database:
- have new items POSTed to it
- have existing items, at /items/ID PUT, PATCHed, and DELETEd
Then you’re all set to use all the functionality in Backbone.Model and Backbone.Collection.
var Item = Backbone.Model.extend({ someMethod: function() { // perform some calculation on the data } }); var Items = Backbone.Collection.extend({ model: Item, });
Nothing is required to define a Model subclass, although you can specify the id attribute name and various other configurables, as well as configuring how the data is pre-processed when it is fetched. Collection subclasses must have a model attribute specifying which Model class is used to instantiate new models from the fetched data.
The huge win with Model and Collection is in their shared sync functionality – persisting their state to the server and letting you know what has changed. It’s also nice being able to attach methods to the model/collection for performing calculations on the data.
Let’s instantiate a collection and fetch it.
var items = new Items(); // Fetch returns a jQuery promise items.fetch().done(function() { items.models // a list of Item objects items.get(4) // get an Item object by its ID });
Easy. Now let’s create a new model instance and persist it to the database:
var newItem = new Item({some: 'data'}); items.add(newItem); newItem .save() .done(function() { messages.success('Item saved'); }) .fail(function() { messages.danger('Item could not be saved lol'); });
Or to fetch models independently:
var item = new Item({id: 5}); item .fetch() .done(function() { someView.render(); }); // or use Events! this.listenTo(item, 'change', this.render); item.fetch();
And to get/set attributes on a model:
var attr = item.get('attributeName'); item.set('attributeName', newValue); // triggers 'change', and 'change:attributeName' events
Backbone.Router and Backbone.History
You’ll have realised already that all the above would work perfectly fine in a typical, SEO-friendly, no-node-required, django views ftw multi-page post-back application. But what about when you want a single page application? For that, backbone offers Router and History.
Router classes map urls to callbacks that typically instantiate View and/or Model classes.
History does little more than detect changes to the page URL (whether a change to the #!page/fragment/
or via HTML 5 pushState) and, upon receiving that event, orchestrates your application’s router classes accordingly based on the urls it detects. It is Backbone.History that takes backbone from the library category to the framework category.
History and Router are typically used together, so here’s some example usage:
var ApplicationRouter = Router.extend({ routes: { 'profile': 'profile', 'items': 'items', 'item/:id': 'item', }, profile: function() { var profile = new ProfileView(); profile.render(); }, items: function() { var items = new Items(); var view = new ItemsView({items: items}); items.fetch().done(function() { items.render(); }); }, item: function(id) { var item = new Item({id: id}) var view = new ItemView({item: item}); item.fetch().done(function() { view.render(); }); } });
Note that above, I’m running the fetch from the router. You could instead have your view render a loading screen and fetch the collection/model internally. Or, in the initialize of the view, it could do this.listenTo(item, 'sync', this.render)
, in which case your routers need only instantiate the model, instantiate the view and pass the model, then fetch the model. Backbone leaves it all to you!
Finally, let’s use Backbone.History to bring the whole thing to life:
$(function(){ // Routers register themselves new ApplicationRouter(); if (someCustomSwitch) { new CustomSiteRouter(); } // History is already instantiated at Backbone.history Backbone.history.start({pushState: true}); });
Now the most common way to use a router is to listen for a click event on a link, intercept the event, and rather than letting the browser load the new page, preventDefault and instead run Backbone.history.navigate('/url/from/link', {trigger: true});
This will run the method associated with the passed route, then update the url using pushState. This is the key: each router method should be idempotent, building as much of the page as required from nothing. Sometimes this method will be called with a different page built, sometimes not. Calling history.navigate will also create a new history entry in the browser’s history (though you can avoid this happening by passing {trigger: true, replace: true}
.
If a user clicks back/forward in the browser, the url will change and Backbone.history will again look up the new url and execute the method associated with that url. If none can be found the event is propagated to the browser and the browser performs a typical page change. In this case, you should be sure in your router method to call .remove
and .undelegateEvents
on any instantiated views that you no longer need or else callbacks for these could still fire. YES, this is incredibly involved.
Finally, you’ll sometimes be in the position where you’ve updated one small part of a page, some sub-view perhaps but you want this change to be reflected in the URL. You don’t necessarily want to trigger a router method because all the work has been done but you do have a router method that could reconstruct the new state of the page were a user to load that page from a full refresh. In this case, you can call Backbone.history.navigate('/new/path');
and it’ll add a new history entry without triggering the related method.
Conclusion
Backbone is unopinionated. It provides an enormous amount of glue code in a way that is very useful and immediately puts you in a much better position than if you were just using jquery. That said, loads more glue code must be written for every application so it could do much, much more. On one hand this means you gain a tremendous amount of flexibility which is extremely useful given the esoteric nature of the applications we build, and it also gives you the power to make your applications incredibly fast, since you aren’t running lots of the general-purpose glue code from much more involved frameworks. It also gives you the power to inadvertently hang yourself.
If you’re looking for something easy to pick up and significantly more powerful than jQuery, but less of a painful risk than the horrible messy world of proper javascript frameworks (see also: the hundreds of angular “regret” blog posts), Backbone is a great place to start.
About us: Isotoma is a bespoke software development company based in York and London specialising in web apps, mobile apps and product design. If you’d like to know more you can review our work or get in touch.