CloudEdit: A Backbone.js Tutorial with Rails (Part 1)

Backbone.js is a javascript MVC framework, and is the newest addition to my frontend toolbox. What's great is that DocumentCloud, the team that released it, is actively developing and using the framework, making it better everyday. I'm currently using it in various projects, including QuietWrite, a javascript-heavy document editing service.

The major advantage of Backbone is that it's simple, lightweight, and gets out of your way, but provides just enough structure to organize large javascript projects.

In this tutorial, I'll go over the code for CloudEdit, an example Backbone.js app backed with Rails that outlines some basic patterns that I've used successfully in my Rails Backbone projects. I'll start by describing the spec for the app, and then detail how the models, controllers, and views hook up. This tutorial assumes you already have some basic knowledge about Rails — I'll be focusing mainly on the Backbone.js concepts.

You can grab all the code for the example app in the GitHub repo. You can also play around with a live version of CloudEdit here.

The Spec

CloudEdit is an extremely simple document editing app. Here are the specs:

  • Users should see a list of the latest documents. To edit, the user clicks the document in the list.
  • Users should be able to edit documents with a title and body, and should be able to save their edits to the server.
  • Users should be able to create new documents.

Directory Structure

First, let's get the directory structure organized. For our Rails project, we have the usual MVC directory structure underneath the apps directory. For the Backbone files, I like to create another set of MVC directories underneath the javascript directory:

app/
    controllers/
        documents_controller.rb
    models/
        document.rb
    views/
        home/
            index.html.erb
public/
    javascripts/
        application.js
        controllers/
            documents.js
        models/
            document.js
        views/
            show.js
            index.js
            notice.js

Notice how the MVC directories mirror the Rails MVC directories.

I like to use Jammit to deliver and package the CSS and Javascript files. It also allows you to specify load ordering. So, for example, you can tell it to load all the vendor javascript files before loading the application specific files.

Let's first go through all the Backbone related code, and then tackle the Rails code (which is much simpler).

Backbone Models

We only have one model: the Document. It has a title and a body attribute. But take note that you don't actually need to specify that in the Backbone model: they're populated by JSON data, either from the server or from the client.

var Document = Backbone.Model.extend({
    url : function() {
      var base = 'documents';
      if (this.isNew()) return base;
      return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + this.id;
    }
});

public/javascripts/models/document.js

The only method defined is the url method, which tells Backbone the URL to persist the Document model when calling save() or destroy(). In this case, it's a pretty typical Rails RESTful model: if it's a new unsaved model, then it should POST to /documents for a CREATE action, and if it's not new, then it should POST to /documents/id for an UPDATE action.

Backbone Controllers

There's 3 actions in our app:

  • Index: Show a list of documents.
  • Edit: Shows a specific document ready to be edited and saved to the server.
  • New: Shows a blank document ready to be created and saved to the server.

Note that these match up with the RESTful pages we would expect from a Document resource. You certainly don't have to organize it this way, but I find that it really helps to have your Backbone structure follow a RESTful pattern.

App.Controllers.Documents = Backbone.Controller.extend({
    routes: {
        "documents/:id":            "edit",
        "":                         "index",
        "new":                      "newDoc"
    },
    
    edit: function(id) {
        var doc = new Document({ id: id });
        doc.fetch({
            success: function(model, resp) {
                new App.Views.Edit({ model: doc });
            },
            error: function() {
                new Error({ message: 'Could not find that document.' });
                window.location.hash = '#';
            }
        });
    },
    
    index: function() {
        $.getJSON('/documents', function(data) {
            if(data) {
                var documents = _(data).map(function(i) { return new Document(i); });
                new App.Views.Index({ documents: documents });
            } else {
                new Error({ message: "Error loading documents." });
            }
        });
    },
    
    newDoc: function() {
        new App.Views.Edit({ model: new Document() });
    }
});

public/javascripts/controllers/documents.js

There's a lot going on here, so let's tackle it piece by piece.

Naming

I like to organize my Backbone controller and views under App.Controllers and App.Views, respectfully. This helps to disambiguate all the object names, especially at the time of instantiation. Thus, our documents controller is App.Controllers.Documents.

Routes

The controller expects you to define routes for each action in your app. In our case, we have the 3 routes we talked about earlier, specified as a javascript object mapping from the url hash structure to the method that is invoked. So, for example, when the user goes to /#documents/3, it will show the edit page for the document with id 3.

Methods

The rule that I use for controller methods is that they should only do 3 things: (1) get the data from the server to populate the models, (2) process that data for the views, and (3) instantiate the views.

For example, in our index method, we make an ajax JSON call to the Documents#index action on the server to get a list of documents. Then, we iterate through the list and instantiate a Document model for each of the JSON data. Finally, we instantiate the App.Views.Index view with the array of models.

The edit method is equally simple: it instantiates the Document with the given id, and then fetches the data for it from the server. Upon success, we instantiate the App.Views.Edit view.

Just like with Rails controllers, it is best to keep them skinny. You should delegate most of the complications to the views and models.

The Main App Object

Now that we have the controller setup, let's actually get our App up and running. The root route for the Rails server will serve up the following HTML, which our Backbone app will use as its scaffolding:

<h1><a href="#">CloudEdit</a></h1>
<h2>A Backbone.js Rails Example by James Yu</h2>

<div id="notice"></div>
<div id="app"></div>

<script type="text/javascript">
    $(function() {
        App.init();
    });
</script>

app/views/home/index.html.erb

The #notice and #app div elements will be used by the app as a scaffold for the UI.

The main App object itself is dead simple:

var App = {
    Views: {},
    Controllers: {},
    init: function() {
        new App.Controllers.Documents();
        Backbone.history.start();
    }
};

public/javascripts/application.js

The two main things to take note is the instantiation of the Documents controller, and the call to the Backbone.history.start(). These combined will kick off the necessary listeners to make the hash routing work.

Backbone Views

Now that we have the plumbing done, let's work on the views, which is the actual user facing interface.

For the purposes of this tutorial, I'll use simple string concatenation in my views. Of course, for more complicated apps, I would highly suggest using a templating framework like jQuery templates or jQote.

Index
App.Views.Index = Backbone.View.extend({
    initialize: function() {
        this.documents = this.options.documents;
        this.render();
    },
    
    render: function() {
        if(this.documents.length > 0) {
            var out = "<h3><a href='#new'>Create New</a></h3><ul>";
            _(this.documents).each(function(item) {
                out += "<li><a href='#documents/" + item.id + "'>" + item.escape('title') + "</a></li>";
            });
            out += "</ul>";
        } else {
            out = "<h3>No documents! <a href='#new'>Create one</a></h3>";
        }
        $(this.el).html(out);
        $('#app').html(this.el);
    }
});

public/javascripts/views/index.js

This view accepts a list of documents, and simply lists them out with links to edit each of the documents. I want to highlight a few things:

  1. Backbone automatically (and conveniently) captures the hash passed to the initialization of the object, and sticks it into this.options.
  2. The only method really required by the views is render, and you can do anything you want to render the view. In this case, all we do is some string concatenation, and stick it into the #app div element.
  3. I like to call render immediately at the end of initialize. What this means is that as soon as you instantiate the view object, it gets rendered.
  4. Note that there is no event delegation specified. The links to each document is handled automatically by the controller by simply specifying the right hash url to the Edit action. Backbone's History object automatically handles routing the clicks to the Edit action. This greatly simplifies views which usually have a million events flying around just to do routing correctly.
Edit/New
App.Views.Edit = Backbone.View.extend({
    events: {
        "submit form": "save"
    },
    
    initialize: function() {
        this.render();
    },
    
    save: function() {
        var self = this;
        var msg = this.model.isNew() ? 'Successfully created!' : "Saved!";
        
        this.model.save({ title: this.$('[name=title]').val(), body: this.$('[name=body]').val() }, {
            success: function(model, resp) {
                new App.Views.Notice({ message: msg });
                
                self.model = model;
                self.render();
                self.delegateEvents();

                Backbone.history.saveLocation('documents/' + model.id);
            },
            error: function() {
                new App.Views.Error();
            }
        });
        
        return false;
    },
    
    render: function() {
        var out = '<form>';
        out += "<label for='title'>Title</label>";
        out += "<input name='title' type='text' />";
        
        out += "<label for='body'>Body</label>";
        out += "<textarea name='body'>" + (this.model.escape('body') || '') + "</textarea>";
        
        var submitText = this.model.isNew() ? 'Create' : 'Save';
        
        out += "<button>" + submitText + "</button>";
        out += "</form>";

        $(this.el).html(out);
        $('#app').html(this.el);
        
        this.$('[name=title]').val(this.model.get('title')); // use val, for security reasons
    }
});

public/javascripts/views/edit.js

I decided to collapse both the Edit and New views into one view: the Edit view. Basically, when a Document is passed in that is newly instantiated (and hasn't been persisted to the server), it acts like a New view, otherwise, it's an Edit view.

The instantiate-and-render pattern is similar to that of the Index view. The new thing that we see here is the event delegation model. Here's how it works:

If you specify an event key on the view object, Backbone will automatically delegate the events. The key to each event is in the form "event selector". In our example, Backbone automatically binds the save method to the form's submit event. This is a very handy shortcut that allows you to organize and peruse events in view easily.

The final thing I want to point out is the save method, which really bears to fruit the power of Backbone. Unlike normal save routines, you don't have to jump through hoops to remember how to persist your data. All of that is already specified in your model. So, all we do is call save on the Document model (with the parameters taken from the title and body form elements), and we're done!

Rails

The great thing about using Backbone is that it makes your server side code very clean. The main meat of it is simply controllers that pass back JSON data to the client via XHR.

Document Model
class Document < ActiveRecord::Base
  attr_accessible :body, :title
  
  def to_json(options = {})
    super(options.merge(:only => [ :id, :title, :created_at, :body ]))
  end
end

app/models/document.rb

The Document model is simple, and only contains a body and title attribute (along with timestamps). One thing to note is that I specifically set body and title to be the only attributes that can be mass assigned, and also specifically set which attributes should be returned when calling to_json. Both of these are for security reasons (you don't want to client to be able to set or receive attributes they don't need).

Documents Controller
class DocumentsController < ApplicationController
  def index
    render :json => Document.all
  end
  
  def show
    render :json => Document.find(params[:id])
  end
  
  def create
    document = Document.create! params
    render :json => document
  end
  
  def update
    document = Document.find(params[:id])
    document.update_attributes! params
    render :json => document
  end
end

app/controllers/documents_controller.rb

As you see, the Rails controller is dead simple: 4 RESTful actions that return data via JSON. No views and no instance variables. This is RESTful design at its finest!

Conclusion and Part 2

Backbone.js really introduces a new kind of data flow for Rails apps. Instead of data flowing like this:

Rails Model => Rails Controller => Rails View

It now flows like this:

Rails Model => Rails Controller => Backbone Model => Backbone Controller => Backbone View

What I've found is that overall, Backbone.js makes it really simple to create highly responsive Javascript heavy applications.

Read on with Part 2 of the Backbone.js and Rails Tutorial, where we'll work with Backbone Collections add Underscore templates. and Part 3 where I show how to convert CloudEdit to use Parse and get rid of the Rails code entirely.

Posted on 27 Jan 2011

James Yu is the co-founder of Parse, lives in San Francisco, and likes to accidentally the whole stack.