Node Tutorial Part 19: Backbone.js

04 Apr 2011 | By Alex Young | Tags server node tutorials lmawa nodepad

Welcome to part 19 of Let’s Make a Web App, a tutorial series about building a web app with Node. This series will walk you through the major areas you’ll need to face when building your own applications. These tutorials are tagged with lmawa.

Click to show previous tutorials.

Backbone.js

Backbone.js is a library for writing client-side JavaScript. It provides base classes for models, collections, and views. If you’ve ever worked on a project with a library like jQuery and found client-side code becomes unwieldy and hard to navigate, Backbone.js can help!

Nodepad’s simple structure means we’re mostly interested in Backbone’s models, collections, and the persistence layer. The persistence layer is the part that talks to the server, communicating using JSON. The views are also useful, however. As Backbone’s documentation says:

It’s all too easy to create JavaScript applications that end up as tangled piles of jQuery selectors and callbacks.

To put it simply, a Backbone application uses models to interact with data, collections to manage sets of models, and views to link events to models and generate dynamic HTML based on templates. The templates are typically hidden HTML, so in our case we’d write stubs in our Jade templates with display: none, then Backbone views would be used to clone and populate these HTML fragments with data.

Planning a Backbone Application

Planning a Backbone application is a bit like planning server-side software — separate out the data from the views and controllers.

Our application consists of:

  1. A document with an ID, title, and body
  2. A document title row
  3. A list of titles which can be selected, added to, and deleted

This already sounds like Backbone primitives:

  1. Backbone.Model, Document: A document with an ID, title, and body
  2. Backbone.View, DocumentRow: A document title row
  3. Backbone.View, DocumentList: A list of titles which can be selected, added to, and deleted
  4. Backbone.Collection, Documents: A collection of documents

Models

Defining models is simple. Use Backbone.Model.extend to create a new model for your application:

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

We can’t actually do very much with this yet, and there also needs to be a collection of models. Collections can be reusable or almost like a singleton (for collections that are used once). In this case we just want one list of documents, so I like to create this type of collection by instantiating a Backbone.Collection:

var Documents = new Backbone.Collection();
Documents.url = '/documents/titles.json';

Each model and collection has a url property — it can be a function or a string. Our collection will always use the same URL. And the reason I’m getting the titles and IDs rather than /documents.json is because we can load the full document on demand.

I usually use functions when I need to use data to construct the URL:

var Document = Backbone.Model.extend({
  Collection: Documents,

  url: function() {
    return '/documents/' + this.get('_id') + '.json';
  }
});

As you can see, when loading a Document the URL will contain our Mongo object’s ID.

There’s something slightly fiddly still left to do. While we could use Documents.fetch() to load the titles, the Backbone authors suggest writing it into the template on the server-side to cut down on the extra Ajax request. We can do this in views/layout.jade like this:

script(type='text/javascript')
  Documents.refresh(!{JSON.stringify(documents)});

The part that reads !{JSON.stringify(documents)} is actually server-side, and Documents.refresh() just overwrites all of the collection’s data in the browser.

Views

The two main views are DocumentRow and DocumentList. The DocumentRow view requires some Jade changes as well. It also uses Backbone.View’s events property to watch for clicks on the document titles:

var DocumentRow = Backbone.View.extend({
  tagName: 'li',

  events: {
    'click a': 'open'
  },

  template: _.template($('#document-row-template').html()),

  initialize: function() {
    _.bindAll(this, 'render');
  },

  open: function() {
    $('#document-list .selected').removeClass('selected');
    $(this.el).addClass('selected');
    this.model.display();
  },

  render: function() {
    $(this.el).html(this.template({
      id: this.model.id,
      title: this.model.get('title')
    }));
    return this;
  }
});

The corresponding Jade template looks like this:

ul#document-list
  li#document-row-template(style='display: none')
    a(id='document_{{ id }}') {{ title }}

The document-row-template part will be hidden and used to generate several instances of DocumentRow. The events property in DocumentRow is Backbone’s convention for mapping events to methods. The left-most part, click is the event, then the rest of the string is used as the selector to observe. The Underscore method, bindAll is used to make sure this refers to an instance of DocumentRow when render is called from an event.

I’m using the standard Underscore _.template method to insert each document’s value:

_.templateSettings = {
  interpolate : /\{\{(.+?)\}\}/g
};

To implement displaying a document it simply populates the form fields:

Document = Backbone.Model.extend({
  Collection: Documents,

  url: function() {
    return '/documents/' + this.get('_id') + '.json';
  },

  display: function() {
    this.fetch({
      success: function(model, response) {
        $('#editor-container input.title').val(model.get('title'));
        $('#editor').val(model.get('data'));
      }
    });
  }
});

Conclusion

I’ve only scratched the surface of Backbone here, but you can see some of the major components of the library. When using Backbone with a larger project it may make sense to keep each class in its own file. In general it can certainly help improve over a monolithic soup of selectors and callbacks.

I’ll continue implementing Nodepad’s client-side code with Backbone next week.

This week’s commit was 8e86240.


blog comments powered by Disqus