Prashant Sahni Blog

Backbone.js todo application with rails as backend

The application is based on this example todo application which uses local storage for backend. The app is created by Jerome Gravel-Niquet.
Addy Osmani is the creator of Backbone.js

Generate the todo model

1
2
3
4
 $ rails g model Todo title:string completed:boolean completed_at:datetime
 $ rake db:migrate

Gemfile

1
2
3
4
5
6
7
8
9
10
source 'https://rubygems.org'
gem 'rails', '3.2.8'
gem 'mysql2'
gem 'jquery-rails'
gem 'backbone-on-rails'
gem 'jasminerice'
gem "ejs", '1.1.1'
gem 'uglifier', '>= 1.0.3'
gem 'execjs'
gem 'therubyracer'
1
 $ bundle install

Read about backbone-on-rails gem here
Backbone-rails is also a very nice gem. You can use it too.

Then,

1
   $ rails generate backbone:install --javascript

What it does it simply creates model, collection, router, view and template path. See the installation code

Here

It requires the needed dependencies in application.js, see 40th line of the installation code.

Directory structure of app folder
Dependencies is a extra directory that i created.

Hope you have basic understanding of Backbone.js.

Here is a very nice book by the creator of Backbone.js.

Now read about underscore.js, javascript templating,

if you are not familiar with it. underscore.js is a dependency of backbone.js.

See one example code using underscore.js EJS.

Embedded JavaScript templates are used in this application.

Read Here

Backbone Todo Model - /assets/javascripts/models/todo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  var app = app || {};
  $(function($){
      'use strict';
      app.Todo = Backbone.Model.extend({
          defaults:{
             title: '',
             completed: false 
          },
          toggle: function() {
              this.url = "/api/todos/" + this.attributes.id + "/completed";
                    this.save({
                    completed: !this.get('completed')
                  });
            }
      });
  });

Backbone todo collection - /assets/javascripts/collectitons/todos.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    var app = app || {};
    $(function($){
        'use strict';
        var TodoList = Backbone.Collection.extend({
           url: "/api/todos" ,
           model: app.Todo,
         completed: function() {
              return this.filter(function( todo ) {
                  return todo.get('completed');
              });
          },
           nextOrder: function() {
              if ( !this.length ) {
                  return 1;
           }
          },
          // Filter down the list to only todo items that are still not finished.
          remaining: function() {
              return this.without.apply( this, this.completed() );
          },
          comparator: function(todo){
              return todo.get('order');
          }

        });

        app.Todos = new TodoList();
    });

May be you are not getting all code into head if you are new. I find it difficult in starting. It takes a bit time to understand it all.

Backbone todo views - /assets/javascripts/views/todo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
var app = app || {};
$(function($){
  'use strict';
  app.TodoView = Backbone.View.extend({
    tagName:  'li',
    template: JST["todos/todo"],
    initialize: function(){
    this.model.on( 'change', this.render, this );
    this.model.on( 'destroy', this.remove, this );
    this.model.on( 'visible', this.toggleVisible, this );
  },
  events:{
    'click .destroy':   'clear' ,
    'keypress .edit':   'updateOnEnter',
    'dblclick label':   'edit',
    'blur .edit':       'restore',
    'click .toggle':    'togglecompleted'
  },
  render: function() {
    this.$el.html( this.template( this.model.toJSON() ) );
    this.$el.toggleClass( 'completed', this.model.get('completed') ); //** Mark if completed **
    this.toggleVisible();                                            //**  Hide or Visible
    this.input = this.$('.edit');
    return this;
  },
  toggleVisible : function () {
    this.$el.toggleClass( 'hidden',  this.isHidden());
  },
  isHidden : function () {
    var isCompleted = this.model.get('completed');
    return ( // hidden cases only
    (!isCompleted && app.TodoFilter === 'completed')
    || (isCompleted && app.TodoFilter === 'active')
    );
  },
  // Remove the item, destroy the item from server and delete its view.
  clear: function() {
    this.model.destroy();
  },
  togglecompleted: function() {
    this.model.toggle();
  },
  // If you hit `enter`, we're through editing the item.
  updateOnEnter: function( e ) {
    if ( e.which === ENTER_KEY ) {
    this.close();
    }
  },
  // Close the `"editing"` mode, saving changes to the todo.
  close: function() {
    var value = this.input.val().trim();
    if ( value ) {
    this.model.save({ title: value});
    } else {
    this.clear();
    }
    this.$el.removeClass('editing');
  },
  // Switch this view into `"editing"` mode, displaying the input field.
  edit: function() {
    this.$el.addClass('editing');
    this.input.focus();
  },
  restore: function(){
    this.$el.removeClass('editing');
  }
 });
})

Backbone todo template - /assets/templates/todos/todo.jst.ejs

1
2
3
4
5
6
 <div class="view">
   <input class="toggle" type="checkbox" <%= completed ? 'checked' : '' %>>
   <label><%- title %></label>
   <button class="destroy"></button>
 </div>
 <input class="edit" value="<%- title %>">

Backbone todo template - /assets/templates/todos/index.jst.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="todoapp">
  <div id="title">
    <h1>Todos</h1>      
  </div>
  <div class="content">
    <div id="create-todo">
      <input id="new-todo" placeholder="What needs to be done?" type="text" />
      <span class="ui-tooltip-top" style="display:none">Press Enter to save this task</span>
    </div>
    <div id="todos">
      <ul id="todo-list"></ul>
    </div>
    <div id="todo-stats"></div>
  </div>
</div>

<ul id="instructions">
  <li>Double-click to edit a todo.</li>
</ul>

Backbone todo template - /assets/templates/todos/stats.jst.ejs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<span id="todo-count"><strong><%= remaining %></strong> <%= remaining === 1 ? 'item' : 'items' %> left</span>
<ul id="filters">
  <li>
    <a class="selected" href="#/">All</a>
  </li>
  <li>
    <a href="#/active">Active</a>
  </li>
  <li>
    <a href="#/completed">Completed</a>
  </li>
</ul>
<% if (completed) { %>
  <button id="clear-completed">Clear completed (<%= completed %>)</button>
<% } %>

Backbone todo views - /assets/javascripts/views/app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
var app = app || {};
$(function($){
  app.AppView = Backbone.View.extend({
    el: '#todoapp',
    statsTemplate: JST['todos/stats'],
    events: {
    'keypress #new-todo': 'createOnEnter',
    'click #clear-completed': 'clearCompleted',
    'click #toggle-all': 'toggleAllComplete'
   },
  initialize: function(){
   this.input = this.$('#new-todo');
   this.allCheckbox = this.$('#toggle-all')[0];
   this.$footer = this.$('#footer');
   this.$main = this.$('#main');

   app.Todos.on( 'add', this.addOne, this );
   app.Todos.on( 'reset', this.addAll, this );
   app.Todos.on( 'change:completed', this.filterOne, this );
   app.Todos.on( 'filter', this.filterAll, this );
   app.Todos.on( 'all', this.render, this );


   app.Todos.fetch(); // -> It is sending request to /api/todos and it will call 'all' and 'reset' method
  },  
  render: function(){
    var completed = app.Todos.completed().length;
    var remaining = app.Todos.remaining().length;

    if ( app.Todos.length ) {
      this.$main.show();
      this.$footer.show();
      this.$footer.html(this.statsTemplate({
      completed: completed,
      remaining: remaining
    }));
    this.$('#filters li a').removeClass('selected')
                           .filter('[href="#/' + ( app.TodoFilter || '' ) + '"]')
                           .addClass('selected');
   } 
   else {
      this.$main.hide();
      this.$footer.hide();
    }
    this.allCheckbox.checked = !remaining;
  },
  // Generate the attributes for a new Todo item.
  newAttributes: function() {
    return {
    title: this.input.val().trim(),
    order: app.Todos.nextOrder(),
    completed: false
   };
  },
  createOnEnter: function( e ) {
    if ( e.which !== ENTER_KEY || !this.input.val().trim() ) {
    return;
  }
  app.Todos.create( this.newAttributes() );
    this.input.val('');
  },
  // **Important Method**
  addOne: function( todo ) {
    var view = new app.TodoView({ model: todo });
      $('#todo-list').append( view.render().el );
    },
// Add all items in the **Todos** collection at once.
  addAll: function() {
    this.$('#todo-list').html('');
    app.Todos.each(this.addOne, this);
  },
  filterOne : function (todo) {
    todo.trigger('visible');
  },
  filterAll : function () {
    app.Todos.each(this.filterOne, this);
  },
  clearCompleted: function() {
    _.each( app.Todos.completed(), function( todo ) {
      todo.destroy();
    });

    return false;
  },

  toggleAllComplete: function() {
    var completed = this.allCheckbox.checked;
    app.Todos.each(function( todo ) {
    todo.save({
      'completed': completed
      });
    });
  }

  });
});

Backbone todo router - /assets/javascripts/routers/router.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var app = app || {};
$(function($) {
    'use strict';
    var Workspace = Backbone.Router.extend({
        routes:{
            '*filter': 'setFilter'
        },
        setFilter: function( param ) {
            // Set the current filter to be used
            app.TodoFilter = param.trim() || '';
            // Trigger a collection filter event, causing hiding/unhiding
            // of Todo view items
            app.Todos.trigger('filter');
        }
    });
    app.TodoRouter = new Workspace();
    Backbone.history.start();
});

Recommended Article to have more understanding -
Where does my javascript code go? Backbone, JST and the Rails 3.1 asset pipeline

Rails Todos Controller - /app/controlles/todos_controller.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class TodosController < ApplicationController

  respond_to :json

  def index
    respond_with Todo.all
  end

  def show
    respond_with Todo.find params[:id]
  end

  def create
    respond_with Todo.create params[:todo]
  end

  def update
    if params["todo_completed"] == "completed"
      respond_with Todo.complete params[:id], params[:todo]
   else
     respond_with Todo.update params[:id], params[:todo]  
    end
  end

  def destroy
    respond_with Todo.destroy params[:id]
  end


end

Rails Todo Model - /app/models/todo.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Todo < ActiveRecord::Base

  attr_accessible :title, :complete_status, :completed


  def self.complete(id, attrs)
    Rails.logger.info("== Completing todo ==")
    todo  = find(id)
    todo.attributes = attrs
    todo.completed_at = Time.now
    todo.save
    return todo
  end


end

Routes

1
2
3
4
5
6
  scope 'api' do
    resources :todos
  end
  match "/api/todos/:id/completed", :to => "todos#update", :todo_completed => "completed"
  root :to => 'home#index'
  match '*path', :to =>  'home#index'

Make a home controller and write its index view as -

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="todoapp">
  <header id="header">
    <h1>todos</h1>
    <input id="new-todo" placeholder="What needs to be done?" autofocus>
  </header>
  <section id="main">
    <input id="toggle-all" type="checkbox">
    <label for="toggle-all">Mark all as complete</label>
    <ul id="todo-list"></ul>
  </section>
  <footer id="footer"></footer>
</div>
<div id="info">
  <p>Double-click to edit a todo</p>
  <p>Written by <a href="https://github.com/addyosmani">Addy Osmani</a></p>
  <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</div>

Kick things off

Make a file todoapp.js - /assets/javascripts/todoapp.js and require it in application.js too

1
2
3
4
5
    var app = app || {};
    var ENTER_KEY = 13;
    $(function() {
      new app.AppView();
    });

Let me know if you face any problem while executing this code. Thanks

comments powered byDisqus