Ember and TodoMVC on Rails

embertodo

With all the attention client side MVC frameworks are getting, I decided it’s time to have a real look at one of them and decide for myself if the hype is worth it.

I’m sure many of you have been playing around with these frameworks while watching others do the same. These short dabbles tell you little about what it’s like to build something useful. In this piece, I will be exploring what it’s like to build something of actual value.

Choosing the right MVC framework may seem difficult. There is Backbone.js, Angular.js, Ember.js just to name a few. The pragmatic Rails developer in me evaluated decided that Ember.js is the most Rails friendly framework. It integrates well with Rails and it’s a good fit when switching from backend to frontend.

To build something interesting and also not re-invent the wheel, we will be building on top of the TodoMVC application. It’s the same application that is used as an example in the official ember guide. Our version will focus on how to build and expand it in the following ways:

  • move to a Rails project
  • use Rails as its backend storage
  • add authentication
  • add private lists for authenticated users
  • add protected sharing of lists

There is a lot of ground to cover, so this will take a couple of posts. Today, we will cover moving the app over to running inside a Rails project and using Rails for backend storage.

TodoMVC in Ember

The TodoMVC application is used as a universal example to compare frontend javascript frameworks. It has just enough functionality to show off the framework while, at the same time, being instantly familiar to any potential developer. Let’s briefly go over the features.

The app displays a list of Todo items with a text field at the top. You can add new items to the list using the text field. Individual items can also be edited by double clicking on them and removed by using a remove icon that shows up when you hover. All the todos can be marked done using the checkbox nex to the input.

Below the list there is a counter of incomplete items and a filter to show all/active/completed tasks. Finally, you can remove all completed items from the list using a ‘clear completed’ button on the bottom.

TodoMVC

This article is not going to over every detail, as there is an excellent article on the official ember guide for that. Here, the focus is on a high level overview of how the parts fit together, making it clear what goes where as we port the example to fit inside a Rails project.

The base template is the place to start to familiarize yourself with an Ember app. This template is where it all comes together: You get an overview (from the script tags) of the size of the application and where things are located. The following excerpt is from the TodoMVC application:

<!doctype html>
<html lang="en" data-framework="emberjs">
  <head>
    <meta charset="utf-8">
    <title>ember.js • TodoMVC</title>
    <link rel="stylesheet" href="bower_components/todomvc-common/base.css">
  </head>
  <body>
    <script type="text/x-handlebars" data-template-name="todos">
    <!--handlebars template content omitted-->
    </script>

    <!--library files-->
    <script src="bower_components/todomvc-common/base.js"></script>
    <script src="bower_components/jquery/jquery.js"></script>
    <script src="bower_components/handlebars/handlebars.js"></script>
    <script src="bower_components/ember/ember.js"></script>
    <script src="bower_components/ember-data/ember-data.js"></script>
    <script src="bower_components/ember-localstorage-adapter/localstorage_adapter.js"></script>

    <!--application files-->
    <script src="js/app.js"></script>
    <script src="js/router.js"></script>
    <script src="js/models/todo.js"></script>
    <script src="js/controllers/todos_controller.js"></script>
    <script src="js/controllers/todo_controller.js"></script>
    <script src="js/views/edit_todo_view.js"></script>
    <script src="js/views/todos_view.js"></script>
    <script src="js/helpers/pluralize.js"></script>
  </body>
</html>

For the most part, it looks like a standard HTML5 document with a lot of javascript. The single non-standard part is the x-handlebars template. The code is omitted here, but is discussed in the official ember guide. Having it inside the HTML like that is fine for small apps, but we’ll be extracting it out as a part of the move to Rails.

The javascript imports are two-fold: The first part is importing library files needed for an Ember app to run, while the other is the Ember app itself. Both of these are discussed in greater detail in the guide, so refer to it for more information.

Setting Up Rails

Rails has good support for hosting Ember applications. All you need to do is include the ember-rails gem in your Gemfile and generate setup files.

gem 'ember-rails'
gem 'ember-data-source', '>= 1.0.0.beta7'

rails g ember:bootstrap

The generator creates an ember folder structure under app/assets/javascripts. The current version is not perfect and some small tweaks are needed to finish setup.

First, remove the original app/assets/javascripts/application.js. Then, add the following two lines to the very top of app/assets/javascripts/application.js.coffee to load jQuery before loading Ember.

#= require jquery
#= require jquery_ujs

To have a root page to open, add the following to the config/routes.rb

Rails.application.routes.draw do
  root to: 'application#index'
end

Also, add an empty app/views/application/index.html.erb. This is a good starting point using the default ApplicationController to render the index action without any more code. Start up the Rails app (rails s) and point the browser to http://localhost:3000 to make sure everything is hooked up.

Moving TodoMVC into Rails

It’s time to copy over the TodoMVC application into our Rails application. The resulting code is on github, if you want to jump to the end.

Start by copying the handlebars template discussed earlier to the app/views/application/index.html.haml. Edit the app/views/layouts/application.html.erb file by removing the turbolinks references and moving javascript_include_tag after the yield inside the body tag. For optional credit, we can remove turbolinks from the Gemfile because we will not be using them.

Complete the migration by copying the following files and converting them to CoffeeScript.

js/routes.js => app/assets/javascripts/routes.js.coffee

TadaEmber.Router.map ->
  @resource 'todos', path: '/', ->
    @route 'active'
    @route 'completed'

TadaEmber.TodosRoute = Ember.Route.extend
  model: -> @store.find('todo')

TadaEmber.TodosIndexRoute = Ember.Route.extend
  setupController: -> @controllerFor('todos').set('filteredTodos', this.modelFor('todos'))

TadaEmber.TodosActiveRoute = Ember.Route.extend
  setupController: ->
    todos = @store.filter 'todo', (todo) ->
      !todo.get('isCompleted')

    @controllerFor('todos').set('filteredTodos', todos)

TadaEmber.TodosCompletedRoute = Ember.Route.extend
  setupController: ->
    todos = @store.filter 'todo', (todo) ->
      todo.get('isCompleted')

    @controllerFor('todos').set('filteredTodos', todos)

js/models/todo.js => app/assets/javascripts/models/todo.js

TadaEmber.Todo = DS.Model.extend
  title: DS.attr('string')
  isCompleted: DS.attr('boolean')

js/controllers/todoscontroller.js => app/assets/javascripts/controllers/todoscontroller.js.cofee

TadaEmber.TodosController = Ember.ArrayController.extend
  actions:
    createTodo: ->
      title = @get('newTitle').trim()
      return if !title

      todo = @store.createRecord 'todo',
        title: title
        isCompleted: false
      todo.save()

      @set('newTitle', '')

    clearCompleted: ->
      completed = @get('completed')
      completed.invoke('deleteRecord')
      completed.invoke('save')

  remaining: Ember.computed.filterBy('content', 'isCompleted', false)
  completed: Ember.computed.filterBy('content', 'isCompleted', true)

  allAreDone: ((key, value) ->
    if value != undefined
      @setEach('isCompleted', value)
      return value;
    else
      length = @get('length')
      completedLength = @get('completed.length')

      return length > 0 && length == completedLength
  ).property('length', 'completed.length')

js/controllers/todocontroller.js => app/assets/javascripts/controllers/todocontroller.js.coffee

TadaEmber.TodoController = Ember.ObjectController.extend
  isEditing: false

  bufferedTitle: Ember.computed.oneWay('title')

  actions:
    editTodo: -> @set('isEditing', true)
    doneEditing: ->
      bufferedTitle = @get('bufferedTitle').trim()
      if Ember.isEmpty(bufferedTitle)
        Ember.run.debounce(@, 'removeTodo', 0)
      else
        todo = @get('model')
        todo.set('title', bufferedTitle)
        todo.save()

      @set('bufferedTitle', bufferedTitle)
      @set('isEditing', false)

    cancelEditing: ->
      @set('bufferedTitle', @get('title'))
      @set('isEditing', false)

    removeTodo: -> @removeTodo()

  removeTodo: ->
    todo = @get('model')
    todo.deleteRecord()
    todo.save()

  saveWhenCompleted: (->
    @get('model').save()
  ).observes('isCompleted')

js/views/edittodoview.js => app/assets/javascripts/views/edittodoview.js.coffee

TadaEmber.EditTodoView = Ember.TextField.extend
  focusOnInsert: (->
    @.$().val(@.$().val())
    @.$().focus
  ).on('disInsertElement')

Ember.Handlebars.helper('edit-todo', TadaEmber.EditTodoView)

js/views/todosview.js => app/assets/javascripts/views/todosview.js.coffee

TadaEmber.TodosView = Ember.View.extend
  focusInput: (-> @.$('#new-todo').focus() ).on('disInsertElement')

js/helpers/pluralize.js => app/assets/javascripts/helpers/pluralize.js

Ember.Handlebars.helper 'pluralize', (singular, count) ->
  inflector = Ember.Inflector.inflector;

  count == 1 ? singular : inflector.pluralize(singular)

app/assets/javascripts/store.js.coffee

TadaEmber.Store = DS.Store.extend()
  # Override the default adapter with the `DS.ActiveModelAdapter` which
  # is built to work nicely with the ActiveModel::Serializers gem.
  #adapter: '_ams'

TadaEmber.ApplicationAdapter = DS.LSAdapter.extend
  namespace: 'tada-emberjs'

Almost done. Copy over bowercomponents/ember-localstorage-adapter/localstorageadapter.js to app/assets/javascript/localstorageadapter.js and add the following line to the top of app/assets/javascript/tadaember.js.coffee

#= require ./localstorage_adapter

Finish the transformation by copying over the contents of the script tag in app/views/application/index.html.erb into the app/javascripts/templates/todos.hbs. Finally, copying the css and images from the original code to our assets directory will add some styling.

Adding Rails at the Backend

The list holds its data inside localstorage of the browser that is currently running the app. Opening the app in another browser will cause the app to reset to a clean state without any todos. We will remedy this by using the Rails app as the storage provider.

First, generate a model and migrate

rails g model Todo title is_completed:boolean
rake db:migrate

Add a controller that will act as an API for the Ember app. Don’t forget about adding a resource call to the router.

app/controllers/todos_controller.rb

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(todo_params)
  end

  def update
    respond_with Todo.update(params[:id], todo_params)
  end

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

  private
    # Never trust parameters from the scary internet, only allow the white list through.
    def todo_params
      params.require(:todo).permit(:title, :is_completed)
    end
end

config/routes.rb

Rails.application.routes.draw do
  resources :todos
  root to: 'application#index'
end

Finally, add a serializer for Rails to properly serialize the model. Ember expects a string ID for every model. The ActiveModelAdapter will handle conversions between the snakecase is_completed comming from the JSON and the camelcase isCompleted that is used in the Ember app.

app/serializers/todo_serializer.rb

class TodoSerializer < ActiveModel::Serializer
  # fix for ember-data deserializer not being able to handle non-string ids
  def id
    object.id.to_s
  end

  attributes :id, :title, :is_completed
end

To take off the training wheels and use the new Rails backend, update the Ember store to use an activemodel store provided by the ember-rails gem. (see this for details.)

TadaEmber.Store = DS.Store.extend
  # Override the default adapter with the `DS.ActiveModelAdapter` which
  # is built to work nicely with the ActiveModel::Serializers gem.
  adapter: '-active-model'

The final product is available on github

Conclusion

We have successfully migrated the TodoMVC app from being a standalone app to running inside Rails. We have also moved away from local storage and are storing data in Rails. In future posts, we’ll tackle adding authentication and the ability to share the list via a url.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.