Beginning Ember.js on Rails: Part 2

January 26, 2012
emberjs, rails, ruby

This is part of a series of posts on Ember.js. Please read Warming up to Ember.js as well as the Ember.js home page if you're new to Ember.js.

As promised in Part 1 of this series, this post will show how the example app uses our ultra-simple Ember REST library to query a resource's REST interface to display a list of records.

Server-side Rails

Our Rails code is close to the minimum needed to serve up a RESTful JSON interface. It was originally generated with rails g scaffold and then simplified further.

Model

The Rails model for contacts is quite basic:

app/models/contact.rb
1
2
3
4
class Contact < ActiveRecord::Base
  validates :first_name, :presence => true
  validates :last_name, :presence => true
end

Controller

The controller is a simplified version of the scaffolding:

app/controllers/contacts_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
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
class ContactsController < ApplicationController
  # GET /contacts
  # GET /contacts.json
  def index
    @contacts = Contact.all

    respond_to do |format|
      format.html # index.html.erb
      format.json { render json: @contacts }
    end
  end

  # GET /contacts/1.json
  def show
    @contact = Contact.find(params[:id])

    respond_to do |format|
      format.json { render json: @contact }
    end
  end

  # POST /contacts.json
  def create
    @contact = Contact.new(params[:contact])

    respond_to do |format|
      if @contact.save
        format.json { render json: @contact, status: :created, location: @contact }
      else
        format.json { render json: @contact.errors, status: :unprocessable_entity }
      end
    end
  end

  # PUT /contacts/1.json
  def update
    @contact = Contact.find(params[:id])

    respond_to do |format|
      if @contact.update_attributes(params[:contact])
        format.json { render json: nil, status: :ok }
      else
        format.json { render json: @contact.errors, status: :unprocessable_entity }
      end
    end
  end

  # DELETE /contacts/1.json
  def destroy
    @contact = Contact.find(params[:id])
    @contact.destroy

    respond_to do |format|
      format.json { render json: nil, status: :ok }
    end
  end
end

There are a couple interesting aspects to this controller code. First of all, the only action that responds to an html request is index. I've trimmed the others to respond only with json.

Also note that the update and destroy actions respond to success with:

1
  render json: nil, status: :ok

... instead of the scaffolded default (head :ok), which jQuery.ajax() interprets as an error.

Routes

As you can see, we're routing root requests to our contacts controller:

config/routes.rb
1
2
3
4
EmberRestExample::Application.routes.draw do
  root :to => 'contacts#index'
  resources :contacts
end

Let's move to the client-side before we get back to our server-side views...

Client-side Ember

We're now going to take a quick look at the client-side Ember model, view and controller code needed to display a list of contacts. This code is all organized within app/assets/javascripts/app/, as discussed in part 1 of this series.

If you're following along in the example code, note that I'm leaving out a lot of the code having to do with editing records for the sake of simplicity.

Application

As reviewed in the first part of this series, the application object can be quite simple:

app/assets/javascripts/app/app.js
1
App = Ember.Application.create();

This object will contain all of the models, controllers and views for the application.

Model

Let's move along to the model:

app/assets/javascripts/app/models/contact.js
1
2
3
4
5
6
7
App.Contact  = Ember.Resource.extend({
  resourceUrl: '/contacts',

  fullName: Ember.computed(function() {
    return this.get('first_name') + ' ' + this.get('last_name');
  }).property('first_name', 'last_name')
});

This is an extension of Ember.Resource, which is defined in ember-rest.js as an extension of Ember.Object. The resourceUrl is the base url of the resource.

Also of interest in the model is the computed property fullName, which returns a concatenation of first_name and last_name. It defines these properties as dependencies with property('first_name', 'last_name'). If either of those dependencies is updated, then fullName will be updated too. Any bindings to fullName in views will also be updated.

Controller

Our Ember-based contacts controller is very simple:

app/assets/javascripts/app/controllers/contacts.js
1
2
3
App.contactsController = Ember.ResourceController.create({
  resourceType: App.Contact
});

Instead of defining a class like App.Contact, we're creating App.contactsController as an object. It's an instance of Ember.ResourceController, which is defined in ember-rest.js as a thin extension to an Ember.ArrayController. Its only configured property is resourceType, which links it to our model. The controller knows enough to use the resourceUrl from the model to query resources (although the resourceUrl can be overridden at the controller level if needed).

Views

Let's now check out our Ember views.

ListContactsView renders the listing:

app/assets/javascripts/app/views/contacts/list.js
1
2
3
4
5
6
7
8
App.ListContactsView = Ember.View.extend({
  templateName:    'app/templates/contacts/list',
  contactsBinding: 'App.contactsController',

  refreshListing: function() {
    App.contactsController.findAll();
  }
});

While ShowContactView renders each row within the listing:

app/assets/javascripts/app/views/contacts/show.js
1
2
3
4
5
App.ShowContactView = Ember.View.extend({
  templateName: 'app/templates/contacts/show',
  classNames:   ['show-contact'],
  tagName:      'tr'
});

Each view is associated with a template via templateName. The rendered template is contained within an html element that is mapped to its corresponding view. By default this is a div, although views can override this with the tagName property. This element can be customized further with classNames.

Also note the contactsBinding property on App.ListContactsView, which binds the controller's data to this view as the property contacts.

Templates

Let's see how each of these views is templated in Handlebars. First up is our listing:

app/assets/javascripts/app/templates/contacts/list.handlebars
1
2
3
4
5
6
7
8
9
10
11
12
13
<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
  {{#each contacts}}
    {{view App.ShowContactView contactBinding="this"}}
  {{/each}}
  </tbody>
</table>

This template loops through contacts, which are bound to App.contactsController, using the each helper and renders each one using ShowContactView. Because ShowContactView has a tagName of tr, a new row will be inserted for each record. It will also render the show template:

app/assets/javascripts/app/templates/contacts/show.handlebars
1
2
3
4
<td>{{contact.id}}</td>
<td class="data">
  {{contact.fullName}}
</td>

Notice how the contactBinding in list.handlebars is passed through to the show view, which can reference the object as contact in its template.

All together now

Let's pull this all together in a single server-rendered ERB view:

app/views/contacts/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
<h1>Contacts</h1>

<script type="text/x-handlebars">
  {{ view App.ListContactsView }}
</script>

<script type="text/javascript">
  $(function() {
    App.contactsController.loadAll(<%= @contacts.to_json.html_safe %>);
  });
</script>

The first script block is written in Handlebars, as indicated by its type. It uses the Ember view helper to render App.ListContactsView.

The javascript block is a bit of an optimization. It loads contacts, fetched in our controller's index action, into our Ember-based App.contactsController. The reason for this is simple: we can improve performance by loading contacts in the same request as the page. A simpler alternative to calling loadAll() would be to find all contacts after jQuery has loaded, like so:

1
2
3
4
5
<script type="text/javascript">
  $(function() {
    App.contactsController.findAll();
  });
</script>

Let's recap what we've done here. We first configured a REST interface to our data in Rails. Next we configured our Ember controller and model to load data from that interface via ember-rest.js. Finally, we hooked up our Ember views and templates to our controller's data.

Let's also recap what we haven't done here. Thanks to ember-rest.js, we haven't needed to write any ajax code to query our REST interface. And thanks to Ember, we haven't needed to write code to update our views when contacts are loaded into the controller's array.

Ember will really begin to shine when we see that our views are kept in sync even as contacts are added, updated, and removed. Speaking of which, my next post will cover just that...


Please continue on to Beginning Ember.js on Rails: Part 3, the final part in this series.