Ember and TodoMVC on Rails
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 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.
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>
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
#= 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
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.
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)
TadaEmber.Todo = DS.Model.extend title: DS.attr('string') isCompleted: DS.attr('boolean')
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')
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')
TadaEmber.EditTodoView = Ember.TextField.extend focusOnInsert: (-> @.$().val(@.$().val()) @.$().focus ).on('disInsertElement') Ember.Handlebars.helper('edit-todo', TadaEmber.EditTodoView)
TadaEmber.TodosView = Ember.View.extend focusInput: (-> @.$('#new-todo').focus() ).on('disInsertElement')
Ember.Handlebars.helper 'pluralize', (singular, count) -> inflector = Ember.Inflector.inflector; count == 1 ? singular : inflector.pluralize(singular)
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'
#= require ./localstorage_adapter
Finish the transformation by copying over the contents of the
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.
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
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.
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
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.