Ember.js Tutorial with Rails 4
The first post in this series, Ember.js Hello World, shows Ember working without a persistence backend. This post covers setting up Rails4 as the persistence engine behind that example, plus adding and deleting records. The amount of Ember and Rails code to make this example is almost completely included in this article. It's that tiny!
The source code for the completed example can be found on GitHub: justin808/ember-js-guides-railsonmaui-rails4. I carefully crafted the commits to explain the steps.
You can try out the application on Heroku at: http://railsonmaui-emberjs-rails4.herokuapp.com/
I put many more details in this comprehensive screencast of how to go from a brand new Rails 4 app to an Ember.js app deployed on Heroku.
Key Tips
- Be sure to update the
ember
andember-data
javascript files with the command from the ember-rails gem (see below). Keeping these files at appropriate versions is key while the API is changing, especially for ember-data. - If you specify the Router property for both
model
andsetupController
, you can have some very confusing results (details below). - Get comfortable with Ember's naming conventions. Ember does a ton with default naming. It's basically got the same philosophy of "Convention over Configuration" of Rails. So it's especially important to try to grok when the Ember examples are doing something implicitly versus explicitly. This is a bit like Rails. At first it seems like magic, like "How the heck is that happening", and then one gets accustomed to the naming conventions and appreciates how much code it saves.
- Be mindful that some Ember.js commands run asynchronously, such as commit.
Building the Hello World without Persistence
The steps for this can be found in the git history up to tag
no-persistence
. Thanks to a few gems, the process is relatively
simple.
Basic Setup
I started off with the instructions here The No Nonsense Guide to Ember.js on Rails. This article covers the basic setup, such as gems to include. You want to pay special attention to the README for ember-rails. Depending on the current state of the ember-rails gem, you may get the deprecation warning (browser console) with the old ember-data.js.
DEPRECATION: register("store", "main") is now deprecated in-favour of register("store:main");
at Object.Container.register (http://0.0.0.0:3000/assets/ember.js?body=1:7296:17)
at Application.initializer.initialize (http://0.0.0.0:3000/assets/ember-data.js?body=1:5069:19)
at http://0.0.0.0:3000/assets/ember.js?body=1:27903:7
at visit (http://0.0.0.0:3000/assets/ember.js?body=1:27041:3)
at DAG.topsort (http://0.0.0.0:3000/assets/ember.js?body=1:27095:7)
at Ember.Application.Ember.Namespace.extend.runInitializers (http://0.0.0.0:3000/assets/ember.js?body=1:27900:11)
at Ember.Application.Ember.Namespace.extend._initialize (http://0.0.0.0:3000/assets/ember.js?body=1:27784:10)
at Object.Backburner.run (http://0.0.0.0:3000/assets/ember.js?body=1:4612:26)
at Object.Ember.run (http://0.0.0.0:3000/assets/ember.js?body=1:5074:26)
Originally, I included a separate version of ember-data in the git repository. Instead, I should have updated the versions of ember and ember-data with this command from the ember-rails README:
rails generate ember:install --head
This command puts the ember files in vendor/assets/ember
. Pretty
sweet. This is way better than manually installing the js files.
Get the no-database fixture example of Ember.js working.
Next, I migrated the non-rails static example presented in Ember.js
Hello
World
to the rails framework. You can checkout the tag no-persistence
and
get the code to where the static fixture is used and there is no
persistence. Scroll to the bottom to see this code, as well as some
additional code added for persistence.
Building the Hello World with Persistence
Create the Model for Blog Posts
You can checkout the git tag persistence-emberjs
to get the git
repository to the state that persistence works.
$ rails generate model Post title:string author:string published_at:date intro:text extended:text
$ rake db:migrate
Since Rails comes pre-configured with sqllite3 by default, no database configuration is required.
Add the Controller and Serializer
Note that in Rails 4, you need to use the form for "strong parameters".
See the definition of post_params
below.
app/models/post.rb
class Post < ActiveRecord::Base
validates_presence_of :published_at, :author
end
app/serializers/post_serializer.rb
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :author, :published_at, :intro, :extended
end
app/controllers/posts_controller.rb
class PostsController < ApplicationController
respond_to :json # default to Active Model Serializers
def index
respond_with Post.all
end
def show
respond_with Post.find(params[:id])
end
def create
respond_with Post.create(post_params)
end
def update
respond_with Post.update(params[:id], post_params)
end
def destroy
respond_with Post.destroy(params[:id])
end
private
def post_params
params.require(:post).permit(:title, :intro, :extended, :published_at, :author) # only allow these for now
end
end
Adding "Add" and "Remove" Buttons
-
To create a new post, use a link, not a button, because we want to change the URL.
-
Don't define both
model
andsetupController
on the Route! If you do, you'll get this error:Uncaught Error: assertion failed: Cannot delegate set('title', a) to the 'content' property of object proxy <App.PostsNewController:ember392>: its 'content' is undefined.
I originally had code like this and it took me some time to figure out that the
model
part was not used.App.PostsNewRoute = Ember.Route.extend( model: -> App.Post.createRecord(publishedAt: new Date(), author: "current user") setupController: (controller) -> # controller.set('content', App.Post.createRecord(publishedAt: new Date(), author: "current user")) )
Update the URL on New with transitionAfterSave Hook
You can't update the URL after a new record is saved directly in the
event handler, as the commit will run asynchronously, and until the
return value, there is no record id, and you would end up using record
id null
in the URL. Here's how to handle this situation. Not that the
save
does the commit, but the transitionToRoute
is not called until
the transitionAfterSave
hook is run.
App.PostsNewController = Ember.ObjectController.extend(
save: ->
@get('store').commit()
transitionAfterSave: ( ->
# when creating new records, it's necessary to wait for the record to be assigned
# an id before we can transition to its route (which depends on its id)
@transitionToRoute('post', @get('content')) if @get('content.id')
).observes('content.id')
)
Don't put the new record, unsaved post in the list of saved posts
There's a slight bug in the adding of new records. If you click on the unsaved post link on the left, the URL will have "null" as the new post does not yet have an ID.
Here's the commit at github, and the commit description:
See discussion at http://stackoverflow.com/questions/14705124/creating-a-record-with-ember-js-ember-data-rails-and-handling-list-of-record Note the change from iterating over "each model" to iterating over "each post in filteredContent" in index.html.erb. That change requires attributes be referenced by "post", and the updated linkTo takes the route, "post", as well as the "dynamic segment" which is also named "post", per the above #each post. (refer to http://emberjs.com/guides/templates/links/). Note the addition of the PostsController. Previously, it was implicitly defined. It listens to property "arrangedContent.@each" so that when the new post saves, the filteredContent property updates and notifies the view template using this property in index.html.erb. Without the listener on this property, the view of all posts would not update.
This is a really important change that is well documented in the commit as well as the tutorial screencast at 36:20.
Heroku Deployment
Heroku has listed many tips at Getting Started with Rails 4.x on
Heroku. And you can look
at the commits leading up to tag heroku
. The basic steps are:
-
Change a few gems
-
Switch from sqllite to postgres.
-
Add a ProcFile to use Puma for the webserver.
-
Be sure that production.rb contains:
config.ember.variant = :production
If you don't, you'll see this error:
RAILS_ENV=production bin/rake assets:precompile rake aborted! couldn't find file 'handlebars' (in /Users/justin/j/emberjs/ember-js-guides-railsonmaui-rails4/app/app/assets/javascripts/application.js:18)
Examples that Inspired this Tutorial
RailsCasts
- The two RailsCasts episodes complement the first tutorial by Tom
Dale by showing how
to add persistence via the
rails-ember
gem. The serializers episode is also useful. - Tip: Using Chrome to watch the videos: I found that the left/right arrow and space bar keys are amazing for pausing and rewinding the RailsCasts so that I could get all the nuances of the Ember naming schemes.
ember_dataexample
- ember_dataexample on GitHub is a nice full featured ember app with a parent child relationship of contacts and phone numbers. It even has some examples of using Konacha for testing Ember JavaScript code.
Source Code for Views and JavaScript
I purposefully kept these to just 2 files to make this example simple. In a real world application, this would be broken into many files.
View Code: app/views/static/index.html.erb
<script type="text/x-handlebars">
<div class="navbar">
<div class="navbar-inner">
<a class="brand" href="#">Bloggr</a>
<ul class="nav">
<li>{{#linkTo 'posts'}}Posts{{/linkTo}}</li>
<li>{{#linkTo 'about'}}About{{/linkTo}}</li>
</ul>
</div>
</div>
{{outlet}}
</script>
<script type="text/x-handlebars" id="about">
<div class='about'>
<p>Justin Gordon wrote this: http://www.railsonmaui.com</p>
<p>Git Repository: </p>
</div>
</script>
<script type="text/x-handlebars" id="posts">
<div class='container-fluid'>
<div class='row-fluid'>
<div class='span3'>
<table class='table'>
<thead>
<tr>
<th>Recent Posts
{{#linkTo "posts.new" class="btn"}}Add Post{{/linkTo}}
</th>
</tr>
</thead>
{{#each post in filteredContent}}
<tr>
<td>
{{#linkTo post post}}{{post.title}}
<small class='muted'>by {{post.author}}</small>
{{/linkTo}}
</td>
</tr>
{{/each}}
</table>
</div>
<div class="span9">
{{outlet}}
</div>
</div>
</div>
</script>
<script type="text/x-handlebars" id="posts/index">
<p class="text-warning">Please select a post</p>
</script>
<script type="text/x-handlebars" id="posts/new">
<legend>Create Post</legend>
{{partial 'post/edit'}}
<button {{action 'save'}} class='btn'>Create</button>
<button {{action cancel}} class='btn'>Cancel</button>
{{partial 'post/view'}}
</script>
<script type="text/x-handlebars" id="post">
{{#if isEditing}}
{{partial 'post/edit'}}
<button {{action 'doneEditing'}} class='btn'>Done</button>
{{else}}
<button {{action 'edit'}} class='btn'>Edit</button>
<button {{action 'delete'}} class='btn'>Delete</button>
{{/if}}
{{partial 'post/view'}}
</script>
<script type="text/x-handlebars" id="post/_view">
<h1>{{title}}</h1>
<h4>by {{author}} <small class="muted">({{date publishedAt}})</small></h4>
<hr>
<div class="intro">
{{markdown intro}}
</div>
<div class="below-the-fold">
{{markdown extended}}
</div>
</script>
<script type="text/x-handlebars" id="post/_edit">
<p>{{view Ember.TextField valueBinding='title' cols="30"}}</p>
<p>{{view Ember.TextArea valueBinding='intro' cols="50"}}</p>
<p>{{view Ember.TextArea valueBinding='extended' cols="80" rows="10"}}</p>
</script>
CoffeeScript: app/assets/javascripts/app.js.coffee.
Here's the entire set of CoffeeScript to build this application. As you can see, it's not much! I intentionally left this in one file to make the example a bit simpler. A real application would break this out into separate files.
App.Store = DS.Store.extend(
revision: 12
adapter: "DS.RESTAdapter" # "DS.FixtureAdapter"
)
App.Post = DS.Model.extend(
title: DS.attr("string")
author: DS.attr("string")
intro: DS.attr("string")
extended: DS.attr("string")
publishedAt: DS.attr("date")
)
App.PostsRoute = Ember.Route.extend(
model: ->
App.Post.find()
)
# See Discussion at http://stackoverflow.com/questions/14705124/creating-a-record-with-ember-js-ember-data-rails-and-handling-list-of-record
App.PostsController = Ember.ArrayController.extend(
sortProperties: [ "id" ]
sortAscending: false
filteredContent: (->
content = @get("arrangedContent")
content.filter (item, index) ->
not (item.get("isNew"))
).property("arrangedContent.@each")
)
App.PostsNewRoute = Ember.Route.extend(
model: ->
App.Post.createRecord(publishedAt: new Date(), author: "current user")
)
App.PostsNewController = Ember.ObjectController.extend(
save: ->
@get('store').commit()
cancel: ->
@get('content').deleteRecord()
@get('store').transaction().rollback()
@transitionToRoute('posts')
transitionAfterSave: ( ->
# when creating new records, it's necessary to wait for the record to be assigned
# an id before we can transition to its route (which depends on its id)
@transitionToRoute('post', @get('content')) if @get('content.id')
).observes('content.id')
)
App.PostController = Ember.ObjectController.extend(
isEditing: false
edit: ->
@set "isEditing", true
delete: ->
if (window.confirm("Are you sure you want to delete this post?"))
@get('content').deleteRecord()
@get('store').commit()
@transitionToRoute('posts')
doneEditing: ->
@set "isEditing", false
@get('store').commit()
)
App.IndexRoute = Ember.Route.extend(redirect: ->
@transitionTo "posts"
)
Ember.Handlebars.registerBoundHelper "date", (date) ->
moment(date).fromNow()
window.showdown = new Showdown.converter()
Ember.Handlebars.registerBoundHelper "markdown", (input) ->
new Ember.Handlebars.SafeString(window.showdown.makeHtml(input)) if input # need to check if input is defined and not null
App.Router.map ->
@resource "about"
@resource "posts", ->
@resource "post",
path: ":post_id"
@route "new"
Conclusion
Ember does quite a lot with just a few lines of code. Definitely check out the source code for the completed example github: justin808/ember-js-guides-railsonmaui-rails4. Please take a look at the screencast, as I put many details beyond this article.
I welcome comments and suggestions.