Press "Enter" to skip to content

Controllers in Ember.js

admin

rocketborder3rev

An excerpt from Developing an Ember.js Edge.

Controllers have two responsibilities in Ember:

  • Manage transient state for sections of the running application. This may involve setting properties on the controller, and having the controller handle some actions.
  • Decorate models for presentation.

Ember encourages the decoupling of application state (“Is the sound muted?”) from the visual representation of that state (“Is the speaker icon crossed out?”). Regardless of if the speaker icon is displayed, the app needs to keep track of the mute state.

A view’s only responsibility is to interface with the DOM. It handles events (like “click” or “drag”), and may modify the DOM directly with jQuery or other libraries. Views are normally quite temporary instances, and ill suited for keeping track of application state. They can cross out the speaker icon when the sound is muted, but once a view is no longer displayed its Ember.View instance is destroyed, losing that state.

Controllers are longer-lived objects that exist regardless of if their information is displayed. Here is an example of storing the isMuted property on a controller, and using it to control the UI:

0001:var App = Ember.Application.create();
0002:App.ApplicationController = Ember.Controller.extend({
0003:  isMuted: false
0004:});
0005:Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
0006:  ''
0007:);

Building on what you should know about Handlebars, the class string will read speaker-icon muted when isMuted is true, and speaker-icon unmated when isMuted is false.

The view object (which renders the template) can be destroyed, and still the controller instance will keep track of the mute state. Another view could attach to the same controller, and show identical information elsewhere on the page and in a different manner. The controller has given us a mechanism to decouple state from how it is displayed.

Changing State with Actions

In these examples, views are generated by Ember. Since there is no need to customize it, there is no need to declare it.

With an application, we can use actions to send messages to the controller. These messages change the application state.

0001:var App = Ember.Application.create();
0002:
0003:Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
0004:  '<i '+ 0005: '{{bind-attr class=":speaker-icon isMuted:muted:unmuted"}}'+ 0006: '{{action "toggleMute"}}'+ 0007: '>'
0008:);
0009:
0010:App.ApplicationController = Ember.Controller.extend({
0011:  isMuted: false,
0012:  actions: {
0013:    toggleMute: function(){
0014:      this.toggleProperty('mute');
0015:    }
0016:  }
0017:});

In this example the ApplicationController is presenting state to the template (as isMuted), and also reacting to messages from the UI requesting that the state be changed. The {{action “toggleMute”}} helper fires an action at the default target of the template, which is wired to be the controller. The controller can then update its state, and via Ember’s bindings the UI is updated to toggle between the “mute” or “unmuted” class.

Controllers can share their state with other controllers, and have parent-child relationships. Actions can bubble up through these relationships, making them a powerful and important part of how complex applications are structured. Near the end of this chapter this is discussed in more detail.

Opening this chapter, it was stated that controllers have two roles. Managing state, and decorating models. Before going into how controllers work in union, let’s cover how they fulfill the decorator pattern.

Decorating A Model With ObjectController

Models represent persisted data for a domain object. While an application is running, there may be properties describing a model that are specific to the application state and should not be persisted. For example:

0001:var profile = Profile.find(yehuda);
0002:
0003:profile.get('name'); // -> Anyone can see Yehuda's name
0004:profile.get('isEditable'); // -> Only Yehuda can edit his name

The name property can be persisted and displayed as it is. The isEditable property is related to the user’s session. Only Yehuda may edit his own profile. This second property is inappropriate to store on the model since it is not persisted and may have a different value in different contexts.

Here is where a decorator becomes useful. The ObjectProxy class provides a way to present the isEditable property as part of a profile without actually storing it on the profile.

0001:var Editable = Ember.ObjectProxy.extend({
0002:  isEditable: false
0003:});
0004:
0005:var profile = Profile.find(yehuda);
0006:
0007:var editableProfile = Editable.create({
0008:  content: profile
0009:});
0010:
0011:// Properties read from the proxy will fall through
0012:// to the `content` property.
0013:
0014:editableProfile.get('isEditable'); // -> is false
0015:editableProfile.get('name'); // -> Yehuda's name
0016:
0017:// Properties are set on the proxy if they are defined
0018:// on the property when it is extended or created.
0019:
0020:editableProfile.set('isEditable', true);
0021:editableProfile.get('isEditable'); // -> is true
0022:profile.get('isEditable'); // -> is undefined. 
      // isEditable is only on the proxy.
0023:
0024:// Properties are set on the content object if they
0025:// are not defined on the proxy.
0026:
0027:editableProfile.set('name', 'Yehuduh');
0028:editableProfile.get('name'); // -> is Yehuduh
0029:profile.get('name'); // -> is Yehuduh

The object proxy is at the core of Ember object controllers. To demonstrate object controllers we will use an application, and this time, a route.

0001:var App = Ember.Application.create();
0002:
0003:Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
0004:  '{{name}}' +
0005:  '{{#if isEditable}}' +
0006:    'edit' +
0007:  '{{/if}}'
0008:);
0009:
0010:App.ApplicationController = Ember.ObjectController.extend({
0011:  model: {
0012:    name: 'Yehuda'
0013:  },
0014:  isEditable: false
0015:});

Note the use of model here instead of content. The two are aliased and interchangeable in object and array controllers.

The properties of name and isEditable can both be accessed by reading right off the controller. There is no need to call model.name to display Yehuda’s name. A more complicated example could not only decorate the model with a new property, but change an existing property.

0001:var App = Ember.Application.create();
0002:
0003:Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
0004:  '{{model.name}} has become the {{name}}!'
0005:);
0006:
0007:App.ApplicationController = Ember.ObjectController.extend({
0008:  model: {
0009:    name: 'Yehuda'
0010:  },
0011:  name: 'Tomhuda'
0012:});

Showing, “Yehuda has become the Tomhuda!” Of course hardcoding models to controllers is less than useful in the real world, so Ember will set the model of a controller base on what is returned from the model hook of it’s route:

0001:var App = Ember.Application.create();
0002:
0003:Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
0004:  '{{name}} is just plain old {{name}}!'
0005:);
0006:
0007:App.ApplicationRoute = Ember.Route.extend(function(){
0008:  model: function(){
0009:    return { name: Yehuda };
0010:  }
0011:});

When an object is returned from the model hook, Ember will generate an ObjectController for you. So {{name}} will be read through the object controller to the model. Often, you may need to show a list of models an not a specific model, and in that case Ember provides a slightly different proxy.

rocketborder.bottom