Kevin Sylvestre

A Ruby and Swift developer and designer.

Setup a Clean Backbone JS App With Rails

Backbone JS is a minimal framework that can be easily integrated into Rails projects. This guide includes some best practices and ideas learned after setting up and working with Backbone for two years on both small and larger projects. Backbone is less convention based than Rails, so use this tutorial as good starting point to develop your own best practices. If you don't know Rails this guide can still be useful (many of the conventions are front end and can be cloned to other frameworks), but some Ruby code is included. This guide also uses CoffeeScript - a fantastic language that compiles into JavaScript.

Setup

To get started add the following gems to your project:

gem 'backbone-on-rails'
gem 'handlebars_assets'
gem 'hamlbars'

Alternatively the assets can be included by downloading and adding the following JS files under vendor/assets/javascripts folder:

Structure

Once the gems are included (and bundle is run), create the following directory structure:

|--app/assets/
|----javascripts/
|------application.js
|------application/
|--------app.coffee
|--------helpers/
|--------templates/
|--------mixins/
|----------base/mixin.coffee
|--------models/
|----------base/model.coffee
|--------collections/
|----------base/collection.coffee
|--------routers/
|----------base/router.coffee
|--------views/
|----------base/view.coffee

In app/assets/javascripts/application.js:

//= require jquery
//= require jquery_ujs
//= require underscore
//= require backbone
//= require handlebars

//= require application/app

In app/assets/javascripts/application/app.coffee:

#= require_self

#= require_tree ./helpers
#= require_tree ./templates

#= require ./mixins/base/mixin
#= require_tree ./mixins/base
#= require_tree ./mixins

#= require ./models/base/model
#= require_tree ./models/base
#= require_tree ./models

#= require ./collections/base/collection
#= require_tree ./collections/base
#= require_tree ./collections

#= require      ./routers/base/router
#= require_tree ./routers/base
#= require_tree ./routers

#= require      ./views/base/view
#= require_tree ./views/base
#= require_tree ./views

@App =
  Cache: {}
  Mixins: {}
  Helpers: {}
  Models: {}
  Collections: {}
  Routers: {}
  Views: {}

_.extend App, Backbone.Events

$ ->
  Backbone.history.start pushState: true

In app/assets/javscripts/application/mixins/base/mixin.coffee:

App.Mixin =

  extend: (mixin) ->
    for key, value of mixin when key not in ['extend','include']
      @[key] = value
    mixin.extended?.apply(@)
    return @

  include: (mixin) ->
    for key, value of mixin when key not in ['extend','include']
      @::[key] = value
    mixin.included?.apply(@)
    return @

In app/assets/javscripts/application/models/base/model.coffee:

class App.Model extends Backbone.Model
_.extend App.Model, App.Mixins

In app/assets/javscripts/application/collections/base/collection.coffee:

class App.Collection extends Backbone.Collection
_.extend App.Collection, App.Mixins

In app/assets/javscripts/application/views/base/view.coffee:

class App.View extends Backbone.View
_.extend App.View, App.Mixins

In app/assets/javascripts/application/routers/base/router.coffee:

class App.Router extends Backbone.Router
_.extend App.Router, App.Mixins

Basics

Now that each of the Backbone JS structures (Models, Collections, Views and Routers) has been 'subclassed' (CoffeeScript uses the class keyword but really this is just a wrapper for prototypal inheritance) new files should inherit from one of the following:

  • App.Model (instead of Backbone.Model)
  • App.Collection (instead of Backbone.Collection)
  • App.View (instead of Backbone.View)
  • App.Router (instead of Backbone.Router)

For example, say that a basic task list is being created. It starts with modifying the main manifest file to include a new 'namespaces':

Modify app/assets/javascripts/application/app.coffee:

...
@App =
  Cache: {}
  Mixins: {}
  Helpers: {}
  Models: {}
  Collections: {}
  Routers: {}
  Views: 
    Tasks: {}

Then create a model app/assets/javascripts/application/models/task.coffee:

class App.Models.Task extends App.Model
  defaults:
    notes: null

Then create a collection app/assets/javascripts/application/collections/tasks.coffee:

class App.Collections.Tasks extends App.Collection
  url: '/tasks'
  model: App.Models.Task

Then define a view app/assets/javascripts/application/views/tasks/index.coffee:

class App.Views.Tasks.Index extends App.View
  render: ->
    @$el.empty()
    for model in @collection.models
      @$el.append(model.get('notes'))

Add a router: app/assets/javascripts/application/routers/tasks.coffee:

class App.Routers.Tasks extends App.Router

  routes:
    "tasks" : "index"

  index: ->
    collection = new App.Collections.Tasks [
      { id: 1, notes: "Dust" }
      { id: 2, notes: "Wash" }
    ]
    view = new App.Views.Tasks.Index(collection: collection)
    $('body').html(view.el)
    view.render()

Finally instantiate the router before starting the Backbone JS history from app/assets/javascripts/application/app.coffee:

...
$ ->
   new App.Routers.Tasks()
   ...

The routes and models will also need to exist in the source (in this case with Rails) application:

rails generate model task notes:string
rails generate controller tasks
rake db:migrate
# config/routes.rb
Rails.application.routes.draw do
  resources :tasks, only: :index
end
# app/controllers/tasks_controller.rb
class TasksController < ApplicationController
  respond_to :html,:json

  # GET /tasks
  def index
    @tasks = Task.all
    respond_with(@tasks)
  end
end
# app/views/tasks/index.html.haml
- provide :title, "Tasks"

If everything is setup properly and the '/tasks' endpoint is loaded the text 'Dust' and 'Wash' will appear. Some of the included gems (handlebars_assets and hamlbars) can be used to help simplify the rendering process. The previous example can be modified slightly to use a template:

Create app/assets/javascripts/application/templates/tasks/index.hamlbars:

.title Tasks
.tasks
  = hb('each .') do
    .task
      .notes= hb('notes')

Modify app/assets/javascripts/application/views/tasks/index.coffee:

class App.Views.Tasks.Index extends App.View
  template: HandlebarsTemplates['tasks/index']

  parameters: ->
    @collection.map (model) ->
      notes: model.get('notes')

  render: ->
    @$el.html(@template(@parameters()))

Syncing

The last step is to setup the application to grab data from the server. Right now the application doesn't support creating tasks (but this can be done from the Rails console):

rails console
Task.create!(notes: "Folding")
Task.create!(notes: "Ironing")

Now it should be possible to verify that the server returns JSON:

curl tasks.dev/tasks.json

[{"id":1,"notes":"Folding","created_at":"2014-11-26T09:30:35.197Z","updated_at":"2014-11-26T09:30:35.197Z"},{"id":2,"notes":"Ironing","created_at":"2014-11-26T09:30:35.997Z","updated_at":"2014-11-26T09:30:35.997Z"}]

Modify app/assets/javascripts/application/views/tasks/index.coffee:

class App.Views.Tasks.Index extends App.View
  ...

  initialize: ->
    @collection.on('sync', @render, @)
    super

  remove: ->
    @collection.off('sync', @render, @)
    super

  ...

Finally app/assets/javascripts/application/routers/tasks.coffee:

class App.Routers.Tasks extends App.Router
  ...

  index: ->
    collection = new App.Collections.Tasks
    view = new App.Views.Tasks.Index(collection: collection)
    $('body').html(view.el)
    collection.fetch()
    view.render()

  ...

Conclusion

That's it! Check some other posts for more Backbone tutorials.