January 31, 2012
emberjs, rails, ruby

Beginning Ember.js on Rails: Part 3

This is part of a series of posts about 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 2 of this series, this post will show how records are added, updated and removed in our example app. This app uses our ultra-simple Ember REST library to communicate with a REST interface served by Rails. I'll try to point out when parts of this example use the persistence library (ember-rest.js) instead of core Ember (ember.js).

Because our REST interface was completed in Part 2, let's move right to the client-side code...

Creating records

We'll start by extending our Ember model and views to enable creating records. As you'll soon see, much of this work is applicable to editing records as well.

Extend the model

In order to serialize our model, we need to define its resourceName and resourceProperties. In this way, ember-rest.js can send back only the data required by our REST interface:

app/assets/javascripts/app/models/contact.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
App.Contact = Ember.Resource.extend({
  resourceUrl:        '/contacts',
  resourceName:       'contact',
  resourceProperties: ['first_name', 'last_name'],

  validate: function() {
    if (this.get('first_name') === undefined || this.get('first_name') === '' ||
        this.get('last_name') === undefined  || this.get('last_name') === '') {
      return 'Contacts require a first and a last name.';
    }
  },

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

The optional validate() method will be called by ember-rest.js when saving records. Returning an error string or object will prevent saving an invalid record. And yes, those validations might look cleaner in CoffeeScript ;)

Extend the listing view

Our listing view needs to allow contacts to be added. Let's modify its template first:

app/assets/javascripts/app/templates/contacts/list.handlebars
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
    </tr>
  </thead>
  <tbody>
  {{#each contacts}}
    {{view App.ShowContactView contactBinding="this"}}
  {{/each}}
  {{#if isNewVisible}}
    <tr>
      <td>*</td>
      <td>
        {{view App.NewContactView}}
      </td>
    </tr>
  {{/if}}
  </tbody>
</table>
<div class="commands">
  <a href="#" {{action "showNew"}}>New Contact</a>
</div>

Notice the "New Contact" link at the bottom. We're using the {{action}} helper to delegate its click event (the default for action) to the showNew() handler on the listing's view class:

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

  showNew: function() {
    this.set('isNewVisible', true);
  },

  hideNew: function() {
    this.set('isNewVisible', false);
  }
});

In turn, the view class sets its isNewVisible property to true. By using set(), Ember's bindings will trigger a change in the above template. {{#if isNewVisible}} will now be true, so the extra row will be displayed in our listing.

Create a view for new contacts

As you can see in the listing's template, a NewContactView will be created within the new row in our listing:

app/assets/javascripts/app/views/contacts/new.js
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
App.NewContactView = Ember.View.extend({
  tagName:      'form',
  templateName: 'app/templates/contacts/edit',

  init: function() {
    this._super();
    this.set("contact", App.Contact.create());
  },

  didInsertElement: function() {
    this._super();
    this.$('input:first').focus();
  },

  cancelForm: function() {
    this.get("parentView").hideNew();
  },

  submit: function(event) {
    var self = this;
    var contact = this.get("contact");

    event.preventDefault();

    contact.saveResource()
      .fail( function(e) {
        App.displayError(e);
      })
      .done(function() {
        App.contactsController.pushObject(contact);
        self.get("parentView").hideNew();
      });
  }
});

There's a lot going on here!

First of all, this view's tagName will make it a form instead of the default div. Next are a couple methods overridden from the standard Ember.View:

init() - This is called to set up a view, but before it gets added to the DOM. It's a good place to create a new contact object and associate it with our view. This object will be referenced in templates associated with our view.

didInsertElement() - This is called immediately after our element gets inserted in the DOM. It's a good place for us to set focus to the initial input element on our form.

There are also a couple custom methods:

cancelForm() - hides this form in our parent view (see hideNew() in the listing view above).

submit() - Ember will automatically set up event listeners for views when you add methods from a standard list (currently here). In this case, we'll be listening for our form's submit() event.

Within the submit() handler, we'll attempt to create our contact with the saveResource() method defined in ember-rest.js. The jQuery deferred methods fail() and done() will handle failure and success. Failure could come from either a client-side validation problem in our model, a problem communicating with the server, or an error returned from our Rails controller. On success, the new contact will be pushed into our controller's collection. There's no need to refresh our views - Ember will just add a new entry to our listing!

Let's check out the template for this view:

app/assets/javascripts/app/templates/contacts/edit.handlebars
1
2
3
4
5
6
7
8
{{#with contact}}
  {{view Ember.TextField valueBinding="first_name" placeholder="First name"}}
  {{view Ember.TextField valueBinding="last_name"  placeholder="Last name"}}
  <button type="submit">
    {{#if id}}Update{{else}}Create{{/if}}
  </button>
{{/with}}
<a href="#" {{action "cancelForm"}}>Cancel</a>

Perhaps you noticed that we aren't applying any attributes from our form to our model before calling saveResource()? We can skip this messiness because we've bound Ember.TextField views to our model's properties. Each view tracks focus and keyboard events to ensure that the model's property stays in sync with its corresponding input field.

You might also notice that, to determine the submit button's text, we're checking the model's id to see if this is a new record to create or an existing one to update. In this way, we can reuse this template to handle edits.

Editing and deleting records

Editing records is very similar to adding them. The major difference is that, in order to enable changes to be saved or cancelled, we're going to edit a copy of our object instead of the object itself.

Because the changes needed to edit and delete records require changes in the same views, I’m going to explain them together.

Extending the show view

Let's extend the template we're using to display each record in our listing:

app/assets/javascripts/app/templates/contacts/show.handlebars
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<td>{{contact.id}}</td>
<td class="data">
  {{#if isEditing}}
    {{view App.EditContactView}}
  {{else}}
    {{contact.fullName}}
  {{/if}}
  <div class="commands">
    {{#unless isEditing}}
      <a href="#" {{action "showEdit"}}>Edit</a>
      <a href="#" {{action "destroyRecord"}}>Remove</a>
    {{/unless}}
  </div>
</td>

You can see that we're following the same pattern from our listing template to either show an EditContactView when isEditing is true, or contact.fullName when it's not. Also, when we're not editing this contact, we display links to edit / remove it.

Let's also extend the corresponding view class:

app/assets/javascripts/app/views/contacts/show.js
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
App.ShowContactView = Ember.View.extend({
  templateName: 'app/templates/contacts/show',
  classNames:   ['show-contact'],
  tagName:      'tr',

  doubleClick: function() {
    this.showEdit();
  },

  showEdit: function() {
    this.set('isEditing', true);
  },

  hideEdit: function() {
    this.set('isEditing', false);
  },

  destroyRecord: function() {
    var contact = this.get("contact");

    contact.destroyResource()
      .fail( function(e) {
        App.displayError(e);
      })
      .done(function() {
        App.contactsController.removeObject(contact);
      });
  }
});

The showEdit() and hideEdit() handlers toggle isEditing, which controls whether EditContactView will be shown within the template (as seen above).

The destroyRecord() handler calls our model's destroyResource() method, defined in ember-rest.js. It has fail() and done() deferreds, just like saveResource(). On success, the record is removed from the controller's collection. As you could guess, Ember ensures that this change is reflected in the DOM immediately.

Extending the edit view

EditContactView is similar to NewContactView, with a few exceptions:

app/assets/javascripts/app/views/contacts/edit.js
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
App.EditContactView = Ember.View.extend({
  tagName:      'form',
  templateName: 'app/templates/contacts/edit',

  init: function() {
    this._super();

    // Create a new contact that's a duplicate of the contact in the parentView;
    // Changes made to the duplicate won't be applied to the original unless
    // everything goes well in submitForm()
    this.set("contact", this.get('parentView').get('contact').copy());
  },

  didInsertElement: function() {
    this._super();
    this.$('input:first').focus();
  },

  cancelForm: function() {
    this.get("parentView").hideEdit();
  },

  submit: function(event) {
    var self = this;
    var contact = this.get("contact");

    event.preventDefault();

    contact.saveResource()
      .fail( function(e) {
        App.displayError(e);
      })
      .done( function() {
        var parentView = self.get("parentView");
        parentView.get("contact").duplicateProperties(contact);
        parentView.hideEdit();
      });
  }
});

Within init(), we create a copy of the parent view's contact. This allows us to make changes that can be discarded if the form is cancelled.

In submit(), the contact's updated properties are copied back to the original after it's been saved successfully. This is done using the duplicateProperties() helper from ember-rest.js.

Of course, if you'd prefer that edits be made directly to the original record, you could skip both of these steps.


Whew, we made it! I hope that this series piqued your interest enough to try Ember yourself. If you've just been reading along, try running and modifying the example app yourself.

Although this is the final part of "Beginning Ember.js on Rails", I plan to blog more about Ember. There's quite a bit more to this framework than I've covered here, so please let me know if you have suggestions for future posts. If you'd like to stay tuned, you can follow our RSS, Twitter and Github accounts in the lower right. Thanks! - Dan