Building an Ember app with RailsAPI - Part 3

This article was last updated on May 28, 2013 and reflects the state of Ember (1.0.0-rc4) and the latest build of Ember Data (0.13) as of that date.

Fork the project on Github!

Use the app live on Heroku

In Part 1 I showed you how to setup a Rails-API app for Ember.

In Part 2 I showed you the basics of building an Ember app, reading from a backend API and displaying that information.

Today we're going to do some coding on the Rails side and the Ember side to add Creating, Updating, and Destroying records.

Part 3 - The Big Finish

In Part 1 we setup the backend using Rails API. In Part 2 we built out the basics of an Ember app, reading from a remote data source and displaying that data. Now we're going to add the ability to Create, Update, and Destroy that data. This part will be a mix of Ember and Rails code.

Note: If you have been following along that Part 2 was recently updated to reflect new changes to the Ember Router, you will need to go back and update your code. Absolute make sure to update your ember.js and ember-data.js dependencies as they have been updated on the github repo

Create

Let's start by adding a Create button to our index page:

1
{{#linkTo 'users.new' class='btn btn-primary'}}Create{{/linkTo}}

We need to add the proper route so the index page doesn't blow up. While we're in here we'll add the edit route as well.

1
2
3
4
5
6
7
App.Router.map ->
  @resource 'users', ->
    @route 'new'
    @route 'edit',
      path: '/:user_id/edit'
    @route 'show',
      path: '/:user_id'

And now we can add the UsersNewRoute

1
2
3
4
5
App.UsersNewRoute = App.UsersRoute.extend
  model: ->
    App.User.createRecord()
  setupController: (controller, model) ->
    controller.set('content', model)

Don't be fooled by the createRecord() call. This will not write anything to the backend. This call is simply used to create a new model. Now let's create the template app/assets/javascripts/templates/users/new.hbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<h1>Create {{fullName}}</h1>
<form>
  <fieldset>
    <div>
      <label {{bindAttr for="firstNameField.elementId"}}>First Name</label>
      {{view Ember.TextField valueBinding='firstName' name='first_name' viewName='firstNameField'}}
    </div>
    <div>
      <label {{bindAttr for='lastNameField.elementId'}}>Last Name</label>
      {{view Ember.TextField valueBinding='lastName' name='last_name' viewName='lastNameField'}}
    </div>
    <div>
      <label {{bindAttr for='quoteField.elementId'}}>Quote</label>
      {{view Ember.TextArea valueBinding='quote' name='quote' viewName='quoteField'}}
    </div>
    <a href='#' {{action save}} class='btn btn-success'>Create</a>
  </fieldset>
</form>
<div class='page-header'></div>

<a href='#' {{action cancel}} class='btn btn-inverse'>Cancel</a>

Next we'll add app/assets/javascripts/controllers/users/newController.coffeescript

1
2
3
4
5
6
7
8
9
10
11
App.UsersNewController = Ember.ObjectController.extend
  headerTitle: 'Create'
  buttonTitle: 'Create'

  save: ->
    @content.save().then =>
      @transitionToRoute('users.show', @content)

  cancel: ->
    @content.deleteRecord()
    @transitionToRoute('users.index')

The first two functions save and cancel are actions that are mapped in the template. Let's break down each:

  • save will make a call to this.store.commit(). You will notice we are not modifying a model, assigning params, etc... as you would in a Rails app. Keep in mind that when you modify data that is bound in the form you are actually modifying the data in the model itself. The datastore in Ember needs to be directed when these modifications should be made "permanent", and because we are using the RESTAdapter Ember will attempt to write these changes to the backend.
  • cancel If the user decides to not create a new user we must delete the record we created then transition to the index page.

Learn more about the Ember Model Lifecycle

Finally we're going to hook up the back end in app/controllers/users_controller.rb

1
2
3
4
5
6
7
8
9
def create
  user = User.new(params[:user])

  if user.save
    render json: user
  else
    render json: user, status: 422
  end
end

It has been mentioned that 422 is the proper status code for validation failures. Personally I would prefer to use respond_with but it isn't part of the default Rails-API stack, hopefully this will change.

Now let's run our app and see how it goes.

New1

Whoops, we have undefined undefined for the fullName. Let's set default values of an empty string in our user model:

1
2
3
4
5
6
7
App.User = DS.Model.extend
  firstName: DS.attr('string', defaultValue: '' )
  lastName: DS.attr('string', defaultValue: '' )
  quote: DS.attr('string')
  fullName: (->
    "#{@get('firstName')} #{@get('lastName')}"
  ).property('firstName', 'lastName')

Now when we add data and create it will write to the back end, take us to the show page. When can then click Back and we can see the record has been automatically added to the collection on the index page.

Adding Edit should be straight forward now that we have done create. Start will adding the route:

1
2
3
4
5
6
App.UsersEditRoute = Ember.Route.extend
  model: (params) ->
    App.User.find(params.user_id)
  setupController: (controller, model) ->
    controller.set('content', model)
    @controllerFor('application').set('currentRoute', 'users')

You'll notice that this route is identical to App.UsersShowRoute we wrote in Part 2, let's DRY this up

1
2
3
4
5
6
7
8
9
App.UserRoute = Ember.Route.extend
  model: (params) ->
    App.User.find(params.user_id)
  setupController: (controller, model) ->
    controller.set('content', model)
    @controllerFor('application').set('currentRoute', 'users')

App.UsersShowRoute = App.UserRoute.extend()
App.UsersEditRoute = App.UserRoute.extend()

Next we'll add the edit link to app/assets/javascripts/templates/users/show.hbs

1
{{#linkTo 'users.edit' content class='btn btn-primary'}}Edit{{/linkTo}}

Now the edit template itself in app/assets/javascripts/templates/users/edit.hbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<h1>Edit {{fullName}}</h1>
<form>
  <fieldset>
    <div>
      <label {{bindAttr for="firstNameField.elementId"}}>First Name</label>
      {{view Ember.TextField valueBinding='firstName' name='first_name' viewName='firstNameField'}}
    </div>
    <div>
      <label {{bindAttr for='lastNameField.elementId'}}>Last Name</label>
      {{view Ember.TextField valueBinding='lastName' name='last_name' viewName='lastNameField'}}
    </div>
    <div>
      <label {{bindAttr for='quoteField.elementId'}}>Quote</label>
      {{view Ember.TextArea valueBinding='quote' name='quote' viewName='quoteField'}}
    </div>
    <a href='#' {{action save}} class='btn btn-success'>Update</a>
  </fieldset>
</form>
<div class='page-header'></div>

<a href='#' {{action cancel target='controller'}} class='btn btn-inverse'>Cancel</a>

And now the controller app/assets/javascripts/controllers/users/editController.coffee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
App.UsersEditController = Ember.ObjectController.extend
  destroy: ->
    @content.deleteRecord()
    @store.commit()
    @transitionTo('users.index')

  save: ->
    @content.save().then =>
      @transitionToRoute('users.show', @content)

  cancel: ->
    if @content.isDirty
      @content.rollback()
    @transitionTo('users.show', @content)

  buttonTitle: 'Edit'
  headerTitle: 'Editing'

This controller looks similar to App.UsersNewController but let's explore the differences

  • save here because the model already has an id we can commit to the datastore and transition.
  • cancel instead of deleting the record we want to rollback to its previous state. And we can only rollback if the record has changed.

I'm sure you know what is next. The new template is nearly identical to the edit template. Let's create app/assets/javascripts/templates/users/form.hbs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<h1>{{headerTitle}} {{fullName}}</h1>
<form>
  <fieldset>
    <div>
      <label {{bindAttr for="firstNameField.elementId"}}>First Name</label>
      {{view Ember.TextField valueBinding='firstName' name='first_name' viewName='firstNameField'}}
    </div>
    <div>
      <label {{bindAttr for='lastNameField.elementId'}}>Last Name</label>
      {{view Ember.TextField valueBinding='lastName' name='last_name' viewName='lastNameField'}}
    </div>
    <div>
      <label {{bindAttr for='quoteField.elementId'}}>Quote</label>
      {{view Ember.TextArea valueBinding='quote' name='quote' viewName='quoteField'}}
    </div>
    <a href='#' {{action save}} class='btn btn-success'>{{buttonTitle}}</a>
  </fieldset>
</form>
<div class='page-header'></div>

<a href='#' {{action cancel target='controller'}} class='btn btn-inverse'>Cancel</a>

And in both the new and edit template remove the markup and replace with

1
{{ template 'users/form' }}

Now we need to edit the two controllers. In App.UsersNewController add to the two attributes:

1
2
headerTitle: 'Create'
buttonTitle: 'Create'

And likewise in App.UsersEditController:

1
2
headerTitle: 'Edit'
buttonTitle: 'Update'

Last part for this section is to add the update action to app/controllers/users_controller.rb:

1
2
3
4
5
6
7
8
def update
  user = User.find(params[:id])
  if user.update_attributes(params[:user])
    render json: user
  else
    render json: user, status: 422
  end
end

Now go through and everything should work! This allows us to treat the templates similar to a partial in Rails.

Finally we're going to add the ability to delete records. Because this is an action we are going to limit to the edit page we will put the link below the render call

1
<a href='#' {{action 'destroy'}} class='btn btn-danger'>Destroy</a>

Now we add the action to the App.UsersEditController

1
2
3
4
destroy: ->
  @content.deleteRecord()
  @store.commit()
  @transitionToRoute 'users.index'

And we add the destroy action to the backend

1
2
3
4
5
6
7
8
def destroy
  user = User.find(params[:id])
  if user.destroy
    render json: user, status: 204
  else
    render json: user
  end
end

The 204 status here is refers to No Content. Ember-data expects this to ensure the destroy action is a success.

That's it! You've just created your very first Ember app with all of the CRUD actions. Congratulations!

Contact us about our design & engineering services!
Tweet

Stay in the know.

Introducing the DockYard Newsletter. Our best design and engineering articles, conveniently delivered to your inbox each month. Sign up below!