InPart 1 of the CloudEdit Backbone.js Tutorial, we developed a basic Rails application usingBackbone.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:
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 theCloudEdit GitHub repo, and also play with thelive app here. They both have been updated to reflect this part of the tutorial.
As you may remember in Part 1, we loaded the list of documents for thedocuments#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 theDocument
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 theApp.Collections
object.
Now, we can update thedocuments#index
action in the Backbone controller as follows:
App.Controllers.Documents=Backbone.Controller.extend({// ... snip ...index:function(){vardocuments=newApp.Collections.Documents();documents.fetch({success:function(){newApp.Views.Index({collection:documents});},error:function(){newError({message:"Error loading documents."});}});},// ... snip ...});
public/javascripts/controllers/documents.js
All we did was instantiate a new instance of theDocuments
collection, and then call fetch with a success callback that passes the collection to theApp.Views.Index
view. We didn't even need to change any Rails code: the original RESTful/documents
action is identical.
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 withhttp://documentcloud.github.com/underscore/ templates, which are powerful and very similar to ERb.
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 globalJST
object that you can use to render your templates into strings. To make Jammit aware of these files, I simply added an entry forapp/views/**/*.jst
in myapp
package inassets.yml
.
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 theApp.Views.Edit
view into thedocument.jst
template. This would turn the following code:
varout='<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>";varsubmitText=this.model.isNew()?'Create':'Save';out+="<button>"+submitText+"</button>";out+="</form>";
into:
<form><labelfor='title'>Title</label><inputname='title'type='text'/><labelfor='body'>Body</label><textareaname='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 themodel
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 inApp.Views.Index
into thedocuments_collection.jst
template. This turns:
if(this.collection.models.length>0){varout="<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><ahref='#new'>Create New</a></h3><ul><% collection.each(function(item) { %><li><ahref='#documents/<%= item.id %>'><%= item.escape('title') %></a></li><% }); %></ul><% } else { %><h3>No documents!<ahref='#new'>Create one</a></h3><% } %>
app/views/documents/documents_collection.jst
Similar to thedocument.jst
template, this template derives all its data from thecollection
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 theApp.Views.Edit
andApp.Views.Index
models, they are now significantly simpler after moving the HTML out.
One last minor cleanup that we'll do is to avoid callingrender
in thesave
method ofApp.Views.Edit
. Instead, we'll bind therender
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 thedocument
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.
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.jstpublic/ 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.
James Yu is the
13 Jun 2013 | Staring at Zero |
29 Nov 2012 | The importance of startup momentum |
23 Nov 2012 | Do everything. Then hire. |
18 Nov 2012 | Startups should think big and start small |
20 May 2012 | Converting CloudEdit from Backbone to Parse in 5 minutes |
13 Jan 2012 | Double Down |
06 Jan 2012 | Hone the Core of Your Product |
31 Dec 2011 | Things I Learned Building a Company in 2011 |
10 Mar 2011 | On Being an Early Startup Employee, and a Farewell |
16 Feb 2011 | Progressive Signup |
09 Feb 2011 | Backbone.js Tutorial with Rails Part 2 |
08 Feb 2011 | Empathetic Product Discovery for Hackers |
05 Feb 2011 | Introducing Gmailr: A Javascript API for Gmail |
27 Jan 2011 | CloudEdit: A Backbone.js Tutorial with Rails (Part 1) |
27 Jan 2011 | Zodiac Hacking: An Accidental SEO Experiment |
26 Jan 2011 | My New Jekyll Blog |
09 Oct 2010 | How My Gap Logo App Became Viral |
31 May 2010 | Tips for the Digital Traveler in Italy |
31 May 2010 | Eating in Italy |
15 Apr 2009 | Foursquare is my Location Memory |
11 Jan 2009 | Why Table Tennis is a Great Hacker Sport |
14 Dec 2008 | A Guide for Your First Usability Test |