Backbone.js Tutorial with Rails Part 2

In Part 1 of the CloudEdit Backbone.js Tutorial, we developed a basic Rails application using Backbone.js that lets users create and edit documents in the cloud. Now, in Part 2, we'll do some refactoring to clean up parts of the app and make things more readable and maintainable.

Specifically, we'll be doing the following:

  • Use Backbone Collections.
  • Use Underscore templating.
  • Use event binding to refresh views.

This update won't change anything in the UI: it's simply some housekeeping to tidy up the code.

As always, you can follow along with the CloudEdit GitHub repo, and also play with the live app here. They both have been updated to reflect this part of the tutorial.

Backbone Collections

As you may remember in Part 1, we loaded the list of documents for the documents#index action via a call to $.getJSON, and then instantiated all the documents in an array. But, we can provide a better abstraction by defining a Backbone Collection as follows:

App.Collections.Documents = Backbone.Collection.extend({
    model: Document,
    url: '/documents'
});

public/javascripts/collections/documents.js

It's pretty simple: we tell the collection that it should hold the Document model (via the model attribute), and that the resource to fetch the documents from the server is located at /documents. Also notice that I'm organizing collections in the same way as the MVC components: the definition is located under the App.Collections object.

Now, we can update the documents#index action in the Backbone controller as follows:

App.Controllers.Documents = Backbone.Controller.extend({
    
    // ... snip ...
    
    index: function() {
        var documents = new App.Collections.Documents();
        documents.fetch({
            success: function() {
                new App.Views.Index({ collection: documents });
            },
            error: function() {
                new Error({ message: "Error loading documents." });
            }
        });
    },
    
    // ... snip ...
    
});

public/javascripts/controllers/documents.js

All we did was instantiate a new instance of the Documents collection, and then call fetch with a success callback that passes the collection to the App.Views.Index view. We didn't even need to change any Rails code: the original RESTful /documents action is identical.

Underscore Templates

Previously, we built up our views using string concatenation. I did this so that we could focus on Backbone.js itself, and not any particular templating language.

However, for anything more than trivial views, string concatenation is a maintenance nightmare. Luckily, http://documentcloud.github.com/jammit/ provides an easy integration with http://documentcloud.github.com/underscore/ templates, which are powerful and very similar to ERb.

.jst Files

Jammit expects your javascript templates (or JST) to live alongside your regular ERb templates as .jst files. It will package up the templates into a global JST object that you can use to render your templates into strings. To make Jammit aware of these files, I simply added an entry for app/views/**/*.jst in my app package in assets.yml.

Convert the Views

Next, we need to convert our views to Underscore templates. This is the fun part, since we get to see the ugly jumble of strings turn into beautiful templates.

Let's first convert the strings in the App.Views.Edit view into the document.jst template. This would turn the following code:

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>";

into:

<form>
    <label for='title'>Title</label>
    <input name='title' type='text' />
    
    <label for='body'>Body</label>
    <textarea name='body'><%= model.get('body') %></textarea>
    
    <button><%= model.isNew() ? 'Create' : 'Save' %></button>
</form>

app/views/documents/document.jst

If you're familiar with ERb templates, this is pretty straightforward. Basically, the template now uses the model object that is passed in to fill in all the data. The call to render this template is:

$(this.el).html(JST.document({ model: this.model }));

No more complicated string concatenation!

Now let's convert the strings in App.Views.Index into the documents_collection.jst template. This turns:

if(this.collection.models.length > 0) {
    var out = "<h3><a href='#new'>Create New</a></h3><ul>";
    this.collection.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>";
}

into:

<% if(collection.models.length > 0) { %>
    <h3><a href='#new'>Create New</a></h3><ul>
    <% collection.each(function(item) { %>
        <li><a href='#documents/<%= item.id %>'><%= item.escape('title') %></a></li>
    <% }); %>
    </ul>
<% } else { %>
    <h3>No documents! <a href='#new'>Create one</a></h3>
<% } %>

app/views/documents/documents_collection.jst

Similar to the document.jst template, this template derives all its data from the collection object that is passed in. We would render it like:

$(this.el).html(JST.documents_collection({ collection: this.collection }));

If you take a look at the App.Views.Edit and App.Views.Index models, they are now significantly simpler after moving the HTML out.

Model Event Binding

One last minor cleanup that we'll do is to avoid calling render in the save method of App.Views.Edit. Instead, we'll bind the render call to any model changes, like so:

App.Views.Edit = Backbone.View.extend({
    
// ... snip ...

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

// ... snip ...

});

Now, whenever the document model changes, the view will be re-rendered. This ensures that the view will always stay up-to-date with the model, no matter what piece of code happens to change it. This is actually fundamental to the philosophy of Backbone, which is to separate the model data from the controllers and views.

Conclusion

Let's take a look at the updated directory structure after these changes:

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

We added two items: the .jst files, and the Backbone collections folder. Overall, the structure is still nicely organized, and its easy to see at a glance how everything connects.

After this update, we have a robust base that we can powerfully extend with more features. What you'll discover is that with Backbone, you avoid a lot of churn that is usually present in persisting data and view fragments in a javascript heavy Rails application. There is now a logical place for all client-side code.

In Part 3, learn about how to convert CloudEdit from Rails to Parse, so you don't need to do any server side coding at all!

Posted on 09 Feb 2011

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