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.