Backbone.js continues to gain popularity in the JavaScript MVC community. I decided to give it a try by creating a simple, single-page app to CRUD a single domain model.
While it wasn’t as trivial as a traditional server-side implementation in Rails, it did turn out relatively clean. This is a long post, and if you make it through it, let me know what you think.
Our server-side is a stock Rails 3.1 app. We’ll go with a classic blog app consisting of a single Post
model to CRUD.
We’ll use the backbone-on-rails Ruby gem to add backbone.js and underscore.js (backbone.js’s only dependency) to our Rails app.
The backbone.js and underscore.js files are not copied into your Rails app’s directory. They’re both automatically loaded by the backbone-on-rails
gem (the jquery-rails
Ruby includes jquery.js and jquery_ujs.js the same way).
Let’s use the scaffold generator from backbone-on-rails
to generate our client-side Backbone.js objects.
We’ll be using CoffeeScript, the Rails default client-side language, throughout our Backbone app. CoffeeScript’s cleaner, Ruby-like syntax will help make the code more readable and understandable.
We’ll start our app by implementing a “Read” action, the homepage. The homepage will be a list of all our posts. The backbone-on-rails
scaffold generator has already created for us a router, an index view and template, a collection, and a model. To wire up the homepage we’ll need to modify our router.
Backbone routers are like controllers in Rails. They map routes to actions. The empty route represents the default action, in our case, the homepage.
app/assets/javascripts/routers/posts_router.js.coffee
In our index
action, we create a Posts
collection, a view for the collection, and then tell the collection to fetch its posts from the server. We don’t tell the view to render itself because the fetch is asynchronous. We’ll see later how the view uses an event handler to render itself after the fetch.
The Posts
collection is configured to make server-side requests to our Rails app at “/posts” and to contain Post
models.
app/assets/javascripts/collections/posts.js.coffee
Our Post
model is as simple as it gets. It’s just a Backbone.Model
subclass and contains no custom logic.
app/assets/javascripts/models/post.js.coffee
Before we take a look at the PostsIndex
view, let’s first quickly go over the Rails layout used on the server-side.
app/views/layouts/application.html.erb
The only change we’ve made to this default Rails layout is the addition of a single “#app” <div>
. This <div>
will act as the container for our app. Each Backbone view will replace its contents.
Now we can take a look at the Backbone view.
app/assets/javascripts/views/posts/posts_index.js.coffee
On initialization, the view binds its render
method as a handler to its collection’s reset
event (this is the Posts
collection we created in the Blog.Routers.Posts#index
action). The Posts
collection will trigger a reset
event after successfully fetching its posts from the server. When the event is fired, the view will render its template and create and render another view for each Post
in its collection.
Here’s the template used by the PostsIndex
view:
app/assets/templates/posts/index.jst.eco
This table will be populated by the view used for each post in the collection:
app/assets/javascripts/views/posts/posts_item.js.coffee
And its template:
app/assets/templates/posts/item.jst.eco
There’s one last thing we need to do before we can view our homepage in a browser: we have to bootstrap our app’s router.
app/assets/javascripts/blog.js.coffee
An onDOMReady
event handler invokes an init
function that creates a PostsRouter
and tells Backbone to start monitoring all hashchange events. All our client-side pages will use hash fragments as URLs. Backbone will listen for hash fragment changes and route them using the routes specified in our PostsRouter
.
Finally we can now start our Rails server and hit our app at http://localhost:3000/posts. Unfortunately, we don’t have any posts yet. So let’s move on to creating a post.
At the bottom of our existing home template is a link to create a new post:
app/assets/templates/posts/index.jst.eco
This means we need to add another route and action to our PostsRouter
.
app/assets/javascripts/routers/posts_router.js.coffee
In our new
action, we create a Post
model, then create a Posts
collection and bind an anonymous handler to its add
event. A collection will fire an add
event whenever a new model is added to it. Our add
event handler performs the equivalent of a server-side redirect by navigating the router to the “empty” router (the homepage). Finally we create and render a view to submit a new post.
app/assets/javascripts/views/posts/posts_new.js.coffee
The element (PostsNew#el
) used in our new post view is the global “#app” <div>
. The new post view will replace the contents of this <div>
with its own template containing a form to submit a new post.
The view also registers a handler to the submit event from the <form>
in its template. This handler stops the default form submission behavior and then asks its posts collection to create a new post. This will result in an HTTP POST request to the server and, if successful, the collection will fire an add
event. This is the same event our PostsRouter
, in its new
action, bound an anonymous “redirect” handler to.
And here is the new post view’s template:
app/assets/templates/posts/new.jst.eco
Great, we can now create posts. However, we can’t let people create posts without a title and a body. Let’s see what we can do to validate our new posts.
Adding some client-side validation will help improve the responsiveness of our UI. We’ll still validate posts on the server-side, but for the user’s sake, I feel this duplication is worth the tradeoff.
Backbone models include a validate
method that can be used to perform custom validations. However, as Rails developers we’re used to ActiveRecord
‘s high-level, declarative approach to validation. And thanks to the jQuery validation plugin, we can have declarative validation on the client-side too.
First, download the plugin (after downloading and unzipping it, remember to restart your Rails server).
Next, add the plugin to our application’s JavaScript manifest file.
app/assets/javascripts/application.js
Now we can add some validation to our new posts.
app/assets/javascripts/views/posts/posts_new.js.coffee
The jQuery validate plugin allows you to specify per-attribute validation rules. Here we’re requiring a title and a body. The validate
method will stop the form submission when validation fails. This will also prevent the new post view’s form submission handler, #create
, from executing as well.
Go and give it a try in the browser. Submitting a post without a title and body will now fail and display an error message(s).
Ok, so far we can display a list of posts, create a post, and validate a post. Next up is: viewing an individual post.
On our home page, each post row included the following link for viewing the post:
app/assets/templates/posts/item.jst.eco
Let’s add a route and action to our router for this post page (i.e., #show
).
app/assets/javascripts/routers/posts_router.js.coffee
This newest route includes a named parameter, :id
. Backbone will automatically pass the named parameter’s value as an argument to our #show
action.
Currently in Backbone, fetching an individual model isn’t very elegant. Above is one of the cleanest ways. You first create a model with the requested id, then add the model to a collection, and finally, tell the model to fetch
itself.
Our #show
action also creates an instance of the following PostsShow
view for the individual post.
app/assets/javascripts/views/posts/posts_show.js.coffee
During initialization, the view adds a handler to its Post
‘s change
event. The Post
will fire a change
event after it has been successfully fetch
ed.
The element (PostsShow#el
) for this view is the same global “#app” <div>
container that was used by the post list and new post views. And like those two views, an individual post view will also replace the contents of this <div>
.
And here’s the posts show view’s template:
app/assets/templates/posts/show.jst.eco
The “back” link goes back to our homepage.
Backbone.js brings the power and maintainability of MVC to the client-side. Routers can be used like Rails server-side controllers to create responsive, single-page apps. Views are also like controllers, but instead of responding to url changes, they respond to DOM level events, such as link clicks and form submissions. Templates contain the app’s markup, they’re what Rails calls views. Models and collections round out Backbone.js, providing traditional class-based, client-side domain modeling.
We’ve only covered the first two letters in CRUD, but it was a lot! I hope this article can give you some direction in implementing the rest of them. The client-side JavaScript MVC space is growing rapidly. Backbone’s quick and easy Rails integration makes it an easy sell but there are other options. Be sure to research other frameworks before evaluating what works best for you.