Introduction

Welcome to the Backbone.js on Rails eBook August 2011 sample. This is published directly from our work-in-progress book, so that you can get a sense for the content, style, and delivery of the product.

We’ve included three sample sections. Two are specific to Rails integration: file organization, and a high-level overview of connecting a Backbone app inside your Rails app. The last is Backbone-specific, and covers filtering and sorting your Backbone collections.

If you enjoy the sample, you can get access to the entire book and sample application as it gets written:

What can I expect from that?

Glad you asked!

The eBook covers intermediate to advanced topics on using Backbone.js, including content specific to integrating with Rails applications. We’re writing it using git-scribe, committing the book into GitHub, and giving you access from day one. This isn’t a finished book yet – but that’s the best part! You get full access to the GitHub repository as we write, so you’ll use the GitHub comment and issue features to give us feedback about what we’ve written and what you’d like to see. Give us your toughest Backbone.js questions, and we’ll see what we can do. Last but not least, also included is a complete sample Backbone.js and Rails application. What the book describes and explains, the example app demonstrates with real, working code. Fully up to date for Rails 3.1.

Contact us

If you have any questions, or just want to get in touch, drop us a line at workshops@thoughtbot.com.

Rails Integration

Organizing your Backbone.js code in a Rails app

When using Backbone.js in a Rails app, you’ll have two kinds of Backbone.js-related assets: classes and templates.

Rails 3.0 and prior

With Rails 3.0 and prior, store your Backbone.js classes in public/javascripts:

public/
  javascripts/
    jquery.js
    jquery-ui.js
    collections/
      users.js
      todos.js
    models/
      user.js
      todo.js
    routers/
      users_router.js
      todos_router.js
    views/
      users/
        users_index.js
        users_new.js
        users_edit.js
      todos/
        todos_index.js

If you are using templates, we prefer storing them in app/templates to keep them separated from the server views:

app/
  views/
    pages/
      home.html.erb
      terms.html.erb
      privacy.html.erb
      about.html.erb
  templates/
    users/
      index.jst
      new.jst
      edit.jst
    todos/
      index.jst
      show.jst

On Rails 3.0 and prior apps, we use Jammit for packaging assets and precompiling templates:

Jammit will make your templates available in a top-level JST object. For example, to access the above todos/index.jst template, you would refer to it as:

JST['todos/index']

Variables can be passed to the templates by passing a Hash to the template, as shown below.

JST['todos/index']({ model: this.model })
Note

A note on Jammit and a JST naming gotcha

One issue with Jammit that we’ve encountered and worked around is that the JST template path can change when adding new templates.

When using Jammit, there is a slightly sticky issue as an app grows from one template subdirectory to multiple template subdirectories.

Let’s say you place templates in app/templates. You work for a while on the "Tasks" feature, placing templates under app/templates/tasks. So, window.JST looks something like:

JST['form']
JST['show']
JST['index']

Now, you add another directory under app/templates, say app/templates/user. Now, all JST references are prefixed with their parent directory name so they are unambiguous:

JST['tasks/form']
JST['tasks/show']
JST['tasks/index']
JST['users/new']
JST['users/show']
JST['users/index']

This breaks existing JST references. You can work around this issue by applying the following monkeypatch to Jammit, in config/initializers/jammit.rb

Jammit::Compressor.class_eval do
  private
  def find_base_path(path)
    File.expand_path(Rails.root.join('app','templates'))
  end
end

As applications are moving to Rails 3.1, they’re also moving to Sprockets for the asset packager. Until then, many apps are using Jammit for asset packaging. We have an open issue and workaround:

Rails 3.1

Rails 3.1 introduces the asset pipeline:

which uses the Sprockets library for preprocessing and packaging assets:

To take advantage of the built-in asset pipeline, organize your Backbone.js templates and classes in paths available to the asset pipeline. Classes go in app/assets/javascripts/, and templates go alongside, in app/assets/templates/:

app/
  assets/
    javascripts/
      collections/
        todos.js
      models/
        todo.js
      routers/
        todos_router.js
      views/
        todos/
          todos_index.js
    templates/
      todos/
        index.jst.ejs
        show.jst.ejs

In Rails 3.1, jQuery is provided by the jquery-rails gem, and no longer needs to be included in your directory structure.

Using Sprockets' preprocessors, we can use templates as before. Here, we’re using the EJS template preprocessor to provide the same functionality as Underscore.js' templates. It compiles the *.jst files and makes them available on the client side via the window.JST object. Identifying the .ejs extension and invoking EJS to compile the templates is managed by Sprockets, and requires the ejs gem to be included in the application Gemfile.

To make the *.jst files available and create the window.JST object, require them in your application.js Sprockets manifest:

//  other application requires
//= require_tree ../templates
//= require_tree .

Additionally, load order for Backbone.js and your Backbone.js app is very important. jQuery and Underscore.js must be loaded before Backbone.js, then the Rails authenticity token patch must be applied. Then your models must be loaded before your collections (because your collections will reference your models) and then your routers and views must be loaded.

Fortunately, sprockets can handle this load order for us. When all is said and done your application.js Sprockets manifest will be as shown below.

//= require jquery
//= require jquery_ujs
//
//= require underscore
//= require backbone
//= require backbone.authtokenadapter
//
//= require example_app
//
//= require_tree ./models
//= require_tree ./collections
//= require_tree ./views
//= require_tree ./routers
//= require_tree ../templates
//= require_tree .

The above is taken from the example application included with this book. You can view it at example_app/app/assets/javascripts/application.js.

Connecting Rails and Backbone.js

By default Backbone.js communicates with your Rails application via JSON gets and posts. If you’ve ever made a JSON API for your Rails app, then for the most part this will be very similar.

If you haven’t ever made a JSON API for your Rails application before, lucky you, it’s pretty straightforward.

Setting Up Rails Models

One important aspect to keep in mind as you plan out how your Backbone.js interface will behave, and how it will use your Rails back-end is that there is no need to have a one-to-one mapping between your Rails models and your Backbone.js models.

The smaller an application is, the more likely that there will be a one-to-one mapping between both Backbone.js and Rails models and controllers.

However, if you have a sufficiently complex application, its more likely that you won’t have a one-to-one mapping due to the differences in the tools Backbone.js gives you and the fact that you’re building a user-interface, not a back-end. Some of the reasons why you won’t have a one to one mapping include:

  • Because you’re building a user interface, not a back-end, it’s likely that some of your backbone models will aggregate information from multiple Rails models into one Backbone.js model.

  • This Backbone.js model may or may not be named the same as one of your Rails models.

  • Backbone.js gives you a new type of object not present in Rails: Collections.

  • Backbone.js doesn’t have the concept of relationships out of the box.

With that said, lets take the simple case first and look at how you might make a Backbone.js version of a Rails model.

In our example application, we have a Task model. The simplest Backbone.js representation of this model would be as shown below.

var Task = Backbone.Model.extend({
  urlRoot: '/tasks'
});

The urlRoot property above indicates to Backbone.js that the server url for instances of this model will be found at /tasks/:id.

In Rails, it’s possible to access individual Tasks, as well as all Tasks (and query all tasks) through the same Task model. However, in Backbone.js models only represent the singular representation of a Task. Backbone.js splits out the plural representation of Tasks into what it calls Collections.

The simplest Backbone.js collection to represent our Tasks would be the following.

var Tasks = Backbone.Collection.extend({
  model: Task
});

If we specify the url for Tasks in our collection instead, then models within the collection will use the collection’s url to construct their own urls, and the urlRoot no longer needs to be specified in the model. If we make that change, then our collection and models will be as follows.

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks'
});

var Task = Backbone.Model.extend({});

Notice in the above model definitions that there is no specification of the attributes on the model. Like ActiveRecord, Backbone.js models get their attributes from the schema and data given to them. In the case of Backbone.js, this schema and data are the JSON from the server.

The default JSON representation of an ActiveRecord model is a Hash that includes all the model’s attributes. It does not include the data for any related models or any methods on the model, but it does include the ids of any related models as those are stored in a relation_name_id attribute on the model.

The JSON representation of your ActiveRecord models will be retrieved by calling to_json on them. You customize the output of to_json by overriding the as_json method in your model. We’ll touch on this more later in the section "Customizing your Rails-generated JSON."

Setting Up Rails Controllers

The Backbone models and collections will talk to your Rails controllers. While your models may not have a one-to-one mapping with their Rails counterparts, it is likely that you’ll have at least one controller corresponding to every Backbone.js model.

Fortunately for us, Backbone.js models will communicate in the normal RESTful way that Rails controllers understand, using the proper verbs to support the standard RESTful Rails controller actions: index, show, create, update, and destroy. Backbone.js does not make any use the new action.

Therefore, it’s just up to us to write a normal restful controller.

There are a few different ways you can write your controllers for interacting with you Backbone.js models and collections. However, the newest and cleanest way is to use the respond_with method introduced in Rails 3.0.

When using respond_with, in your controller you specify what formats are supported with the method respond_to. In your individual actions, you then specify the resource or resources to be delivered using respond_with, as shown in the example Tasks controller and index action below.

class TasksController < ApplicationController::Base
  respond_to :html, :json

  def index
    respond_with(@tasks = Task.all)
  end
end

In the above example Tasks controller, the respond_to line declares that this controller should respond to both the HTML and JSON formats. Then, in the index action, the respond_with call will perform the appropriate action for the requested format.

The above controller is equivalent to the following one, using the older respond_to method.

class TasksController < ApplicationController::Base
  def index
    @tasks = Task.all
    respond_to do |format|
      format.html
      format.json { render :json => @tasks }
    end
  end
end

Using respond_with you can create succinct controllers that respond with a normal web page, but also expose a JSON api that Backbone.js will use.

Validations and your HTTP API

If a Backbone.js model has a validate method defined, it will be validated before its attributes are set. If validation fails, no changes to the model will occur, and the "error" event will be fired. Your validate method will be passed the attributes that are about to be updated. You can signal that validation passed by returning nothing from your validate method. You can signify that validation has failed by returning something from the method. What you return can be as simple as a string, or a more complex object that describes the error in all its gory detail.

In practice, much of the validation logic for your models will continue to be handled on the server, as fully implementing validations on the client side would often require duplicating a lot of server-side business logic.

TODO: Is it possible to smoothly integrate Backbone.js and the client_side_validations gem?

Instead, your Backbone.js applications will likely rely on server-side validation logic. How to handle a failure scenario is passed in to Backbone.js model save call as a callback, as shown below.

task.save({title: "New Task title"}, {
  error: function(){
    // handle error from server
  }
});

The error callback will be triggered if your server returns a non-200 response. Therefore, you’ll want your controller to return a non-200 HTTP response code if validations fail.

A controller that does this would be as shown in the following example.

class TasksController < ApplicationController::Base
  respond_to :json

  def create
    @task = Task.new(params[:task])
    if @task.save
      respond_with(@task)
    else
      respond_with(@task, :status => :unprocessable_entity)
    end
  end
end

Your error callback will receive both the model as it was attempted to be saved and the response from the server. You can take that response and handle the errors returned by the above controller in whatever way is fit for your application. For more information about handling and displaying errors, see the Form helpers section of the Views and Templates chapter.

Setting Up Views

Most Backbone.js applications will be a "single-page app". This means that your Rails application will render a single-page which properly sets up Backbone.js and the data it will use. From there, ongoing interaction with your Rails application occurs via the JSON apis.

The most common page for this single-page application will be the index action of a controller, as in our example application and the tasks controller.

You will want to create an object in Javascript for your Backbone.js application to reside. For more information on this namespacing see the "Namespacing your application" section of the Organization chapter.

This namespace variable holds your Backbone.js application’s Models, Collections, Views, and Routes, and has an init method which will be called to initialize the application.

This namespace variable will look like the following.

var ExampleApp = {
  Models: {},
  Collections: {},
  Views: {},
  Routers: {},
  init: function() {
    new ExampleApp.Routers.Tasks();
    Backbone.history.start();
  }
};

You can find this file in the example app in app/assets/javascripts/example_app.js.

Important
You must instantiate a Backbone.js router before calling Backbone.history.start() otherwise Backbone.history will be undefined.

Then, inside app/views/tasks/index.html.erb you will call the initialize method. This will appear as follows.

<%= content_for :javascript do -%>
  <%= javascript_tag do %>
    ExampleApp.init();
  <% end %>
<% end -%>

For performance reasons, you will almost always "prime the pump" and give Backbone.js its initial data within the HTML view for this page. In our example, the tasks have already been provided to the view in a @tasks instance variable, and that can be used to prime the pump, as shown below.

<%= content_for :javascript do -%>
  <%= javascript_tag do %>
    ExampleApp.init(<%= @tasks.to_json %>);
  <% end %>
<% end -%>

The above example uses Erb to pass the JSON for the tasks to the init method.

Once you make this change, the ExampleApp.init method then becomes:

var ExampleApp = {
  Models: {},
  Collections: {},
  Views: {},
  Routers: {},
  init: function(tasks) {
    new ExampleApp.Routers.Tasks();
    this.tasks = new ExampleApp.Collections.Tasks(tasks);
    Backbone.history.start();
  }
};

Finally, you must have a Router in place which knows what to do. We’ll cover routers in more detail in the Views and Templates chapter. For a more in-depth presentation on writing and using routes please go there. However, routers are an important part of the infrastructure you need to start using Backbone.js and we can’t make our example here work without them.

Backbone.js routers provide methods for routing application flow based on client-side URL fragments (#fragment).

ExampleApp.Routers.Tasks = Backbone.Router.extend({
  routes: {
    "": "index"
  },

  index: function() {
    // We've reached the end of Rails integration - it's all Backbone from here!

    alert('Hello, world!  This is a Backbone.js router action.');

    // Normally you would continue down the stack, instantiating a
    // Backbone.View class, calling render() on it, and inserting its element
    // into the DOM.
  }
});

A basic router consists of a routes hash which is a mapping between url fragments and methods on the router. If the current URL fragment, or one that is being visited matches one of the routes in the hash, its method will be called.

The example router above is all that is needed to complete our Backbone.js infrastructure. When a user visits /tasks the index.html.erb view will be rendered which properly initialized Backbone.js and its dependencies and the Backbone.js models, collections, routers, and views.

Models and Collections

Filters and sorting

When using our Backbone models and collections, it’s often handy to filter the collections by resuable criteria, or sort them by several different criteria.

Filters

To filter a Backbone.Collection, like with Rails named scopes, define functions on your collections that filter by your criteria, using the select function from Underscore.js, and return new instances of the collection class. A first implementation might look like this:

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  complete: function() {
    var filteredTasks = this.select(function(task) {
      return task.get('completed_at') !== null;
    });
    return new Tasks(filteredTasks);
  }
});

Let’s refactor this a bit. Ideally, the filter functions will reuse logic already defined in your model class:

var Task = Backbone.Model.extend({
  isComplete: function() {
    return this.get('completed_at') !== null;
  }
});

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  complete: function() {
    var filteredTasks = this.select(function(task) {
      return task.isComplete();
    });
    return new Tasks(filteredTasks);
  }
});

Going further, notice that there are actually two concerns in this function. The first is the notion of filtering the collection, and the other is the specific filtering criteria (task.isComplete()).

Let’s separate the two concerns here, and extract a filtered function:

var Task = Backbone.Model.extend({
  isComplete: function() {
    return this.get('completed_at') !== null;
  }
});

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  complete: function() {
    return this.filtered(function(task) {
      return task.isComplete();
    });
  },

  filtered: function(criteriaFunction) {
    return new Tasks(this.select(criteriaFunction));
  }
});

We can extract this function into a reusable mixin, abstracting the Tasks collection class using this.constructor:

FilterableCollectionMixin = {
  filtered: function(criteriaFunction) {
    return new this.constructor(this.select(criteriaFunction));
  }
};

var Task = Backbone.Model.extend({
  isComplete: function() {
    return this.get('completed_at') !== null;
  }
});

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  complete: function() {
    return this.filtered(function(task) {
      return task.isComplete();
    });
  }
});

_.extend(Tasks, FilterableCollectionMixin);

Sorting

The simplest way to sort a Backbone.Collection is to define a comparator function. This functionality is built in:

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  comparator: function(task) {
    return task.dueDate;
  }
});

If you’d like to provide more than one sort order on your collection, you can use an approach similar to the filtered function above, and return a new Backbone.Collection whose comparator is overridden. Call sort to update the ordering on the new collection:

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  comparator: function(task) {
    return task.dueDate;
  },

  byCreatedAt: function() {
    var sortedCollection = new Tasks(this.models);
    sortedCollection.comparator = function(task) {
      return task.createdAt;
    };
    sortedCollection.sort();
    return sortedCollection;
  }
});

Similarly, let’s extract the resuable concern to another function:

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  comparator: function(task) {
    return task.dueDate;
  },

  byCreatedAt: function() {
    return this.sortedBy(function(task) {
      return task.createdAt;
    });
  },

  byCompletedAt: function() {
    return this.sortedBy(function(task) {
      return task.completedAt;
    });
  },

  sortedBy: function(comparator) {
    var sortedCollection = new Tasks(this.models);
    sortedCollection.comparator = comparator;
    sortedCollection.sort();
    return sortedCollection;
  }
});

And then into another reusable mixin:

var SortableCollectionMixin = {
  sortedBy: function(comparator) {
    var sortedCollection = new this.constructor(this.models);
    sortedCollection.comparator = comparator;
    sortedCollection.sort();
    return sortedCollection;
  }
};

var Tasks = Backbone.Collection.extend({
  model: Task,
  url: '/tasks',

  comparator: function(task) {
    return task.dueDate;
  },

  byCreatedAt: function() {
    return this.sortedBy(function(task) {
      return task.createdAt;
    });
  },

  byCompletedAt: function() {
    return this.sortedBy(function(task) {
      return task.completedAt;
    });
  }
});

_.extend(Tasks.prototype, SortableCollectionMixin);

Closing

Thanks for checking out the August 2011 sample of our Backbone.js on Rails eBook.

If you’d like to get access to the full content during the entire writing process, with regularly published updates and the fullly completed work at the end, you can pick it up on our website: