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.
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.
Loves crafting apps that are usefull and beautifly coded while teaching others how to do the same. When not coding he likes mountain biking and all sports that involve speed.