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

  1. Be sure to update the ember and ember-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.
  2. If you specify the Router property for both model and setupController, you can have some very confusing results (details below).
  3. 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.
  4. 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:

1
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.

1
2
$ 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

1
2
3
class Post  ActiveRecord::Base
  validates_presence_of :published_at, :author
end

app/serializers/post_serializer.rb

1
2
3
class PostSerializer  ActiveModel::Serializer
  attributes :id, :title, :author, :published_at, :intro, :extended
end

app/controllers/posts_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
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 and setupController 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 : 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.

    1
    2
    3
    4
    5
    6
    
    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.

1
2
3
4
5
6
7
8
9
10
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:

  1. Change a few gems
  2. Switch from sqllite to postgres.
  3. Add a ProcFile to use Puma for the webserver.
  4. Be sure that production.rb contains:
    1
    
    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_data_example

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

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














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.

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
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.


This is a companion discussion topic for the original entry at http://www.railsonmaui.com//blog/2013/06/11/emberjs-rails4-tutorial/

Hi Aaron,

Thanks for the kind words. I have not yet looked into the issue you mention, but that's a common issue for any multi-user system. One thing you could do is to refresh the controller on a timer. That's how CampFire works, for example. DHH spoke of that in a talk i saw on youtube. In terms of two windows, are they sharing the same JavaScript? And the same session? If so, then could you use the same EmberController and EmbeData objects?

Hey Justin, thanks for the great articles on Ember.js , they're very helpful.

Do you have any thoughts on the current Ember Data and how it handles stale data? For example, if you open two windows and delete a record in one window, the other window doesn't show that the record was deleted unless you do a page refresh. I'm currently getting around this by creating my own find function in my models which use ajax to create Ember ArrayProxy's and populate them with Ember Objects. A bit hacky, and I lose my bindings, but it works.

The No Nonsense Guide appears to have vanished...or at any rate is currently unavailable at that host.

Man this is great - thanks so much for taking the time to make these videos!

Link is still there as of this morning.

Chris, you're most welcome. I love getting any feedback, even if it's just "This was helpful". Thanks!

Great Info. Thanks for the your effort. Appreciate it.

http://mobisoftinfotech.com/se...

awesome article
u a geek man