English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

Developing an Ember.js Edge

 7. Ember.Controller

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 whether 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 whether their information is displayed. Here is an example of storing the isMuted property on a controller, and using it to control the UI:


var App = Ember.Application.create();
App.ApplicationController = Ember.Controller.extend({
isMuted: false
});
Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'<i {{bind-attr class=":speaker-icon isMuted:muted:unmuted"}}></i>'
);

Building on what you should know about Handlebars, the class string will read speaker-icon muted when isMuted is true, and speaker-icon unmuted 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.


var App = Ember.Application.create();

Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'<i '+
'{{bind-attr class=":speaker-icon isMuted:muted:unmuted"}}'+
'{{action "toggleMute"}}'+
'></i>'
);

App.ApplicationController = Ember.Controller.extend({
isMuted: false,
actions: {
toggleMute: function(){
this.toggleProperty('mute');
}
}
});

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 is specific to the application state and should not be persisted. For example:


store.find('profile', 'yehuda').then(function(profile){
profile.get('name'); // -> Anyone can see Yehuda's name
profile.get('isEditable'); // -> Only Yehuda can edit his name
});

The name property can be persisted and displayed as 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.


var Editable = Ember.ObjectProxy.extend({
isEditable: false
});

store.find('profile', 'yehuda').then(function(profile){
var editableProfile = Editable.create({
content: profile
});

// Properties read from the proxy will fall through
// to the `content` property.

editableProperty.get('isEditable'); // -> is false
editableProfile.get('name'); // -> Yehuda's name

// Properties are set on the proxy if they are defined
// on the property when it is extended or created.

editableProfile.set('isEditable', true);
editableProfile.get('isEditable'); // -> is true
profile.get('isEditable'); // -> is undefined. isEditable is only on the proxy.

// Properties are set on the content object if they
// are not defined on the proxy.

editableProfile.set('name', 'Yehuduh');
editableProfile.get('name'); // -> is Yehuduh
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.


var App = Ember.Application.create();

Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'<span>{{name}}</span>' +
'{{#if isEditable}}' +
'<a href="#" {{action "edit"}}>edit</a>' +
'{{/if}}'
);

App.ApplicationController = Ember.ObjectController.extend({
model: {
name: 'Yehuda'
},
isEditable: false
});

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.


var App = Ember.Application.create();

Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'{{model.name}} has become the {{name}}!'
);

App.ApplicationController = Ember.ObjectController.extend({
model: {
name: 'Yehuda'
},
name: 'Tomhuda'
});

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 based on what is returned from the model hook of its route:


var App = Ember.Application.create();

Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'{{name}} is just a plain old {{name}}!'
);

App.ApplicationRoute = Ember.Route.extend({
model: function(){
return { name: 'Yehuda' };
}
});

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

Decorating Many Models With ArrayController

An array controller proxies many models as an array instead of a single model. It provides the opportunity to decorate both the array itself and the items in that array. For example, an array controller could manage the knowledge about which item in an array is selected.


var App = Ember.Application.create();

Ember.TEMPLATES['application'] = Ember.Handlebars.compile(
'{{#if selectedItem}}{{selectedItem.name}} is selected{{/if}}' +
'{{#each}}{{name}}{{/each}}!'
);

App.ApplicationRoute = Ember.Route.extend({
model: function(){
return [
{ name: 'Arabica' },
{ name: 'Robusta' },
{ name: 'Barako' }
];
}
});

App.ApplicationController = Ember.ArrayController.extend({
selectedItem: null
});

Similar to how Ember.ObjectController is based on Ember.ObjectProxy, the Ember.ArrayController is based on the Ember.ArrayProxy.

This example has taken us across a new boundary. The template in this last example has two scopes. The first is the scope of the application controller, and it is how selectedItem is found. The second is inside the {{#each}} block, where a single item became the scope for that section of the template. What if we wanted to decorate that model? Or have some state for each of the items as they are displayed?

Ember's variant of Handlebars provides several helpers for referencing other templates and controllers. When you use these helpers, you start creating hierarchies of controllers. First we will take a look at how to use some of these helpers, then investigate how to share state between controllers.

Template Scope and Controllers

Templates in Ember are nearly always backed by controllers. When a template is rendered by a route, the router wires the template to a controller with the same name. Because controllers are how you decorate a model in Ember, it can be useful to change which controller is backing a template (or fragment of a template). For instance, consider a UI looping over a list of items:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return {
name: "Richard",
sisters: [ {name: "Kerri"}, {name: "June"} ]
};<br /> }
});
App.IndexController = Ember.ObjectController.extend({
name: function(){
return "Mr. "+this.get('model.name');
}.property('mode.name')
});
Ember.TEMPLATES["index"] = Ember.Handlebars.compile(
"{{name}} has sisters: <ul>{{#each sisters}}<li>{{name}}</li>{{/each}}</ul>"
);

This application would display:


Mr. Richard has sisters: <ul><li>Kerri</li><li>June</li></ul>

The each helper forces the creation of a new view. This view chooses a context for its template, and by default it will simply pass the iterated value as the context. In this case, it would pass {name: "Kerri"} and {name: "June"} to each of its child views.

#each will accept an argument to configure a controller for each item.


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return {
name: "Richard",
sisters: [ {name: "Kerri"}, {name: "June"} ]
};<br /> }
});

App.IndexController = Ember.ObjectController.extend({
name: function(){
return "Mr. "+this.get('model.name');
}.property('mode.name')
});
App.MsController = Ember.ObjectController.extend({
name: function(){
return "Ms. "+this.get('model.name');
}.property('mode.name')
});
Ember.TEMPLATES["index"] = Ember.Handlebars.compile(
"{{name}} has sisters: <ul>" +
'{{#each sisters itemController="ms"}}<li>{{name}}</li>{{/each}}' +
"</ul>"
);

Now Ember renders:

Mr. Richard has sisters: <ul><li>Ms. Kerri</li><li>Ms. June</li></ul>

The itemController argument causes {{#each to pass items to a controller, which can decorate and present them.

The render helper can accept a single object to present, or present no object at all.


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function(){
return {
name: "Richard",
sister: { name: "Kerri" }
};
}
});
App.IndexController = Ember.ObjectController.extend({
name: function(){
return "Mr. "+this.get('model.name');
}.property('model.name')
});
App.SisterController = Ember.ObjectController.extend({
name: function(){
return "Ms. "+this.get('model.name');
}.property('model.name')
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'{{name}} has a sister named {{render "sister" sister}}'
);
Ember.TEMPLATES['sister'] = Ember.Handlebars.compile(
'<span class="sister">{{name}}</span>'
);

These examples demonstrate how helpers change the scope of a template to a new controller, but controllers have a dual responsibility of presentation and managing state. Template helpers also tie controllers together in a tree, structuring how they can access each other and pass actions.

Actions and Controllers

Routes and scope-changing helpers have an important impact on an application's architecture. They change how actions bubble. In the chapter on Handlebars we quickly explained the actions helper. The action helper captures a DOM event, then sends a message to the controllers and routes of an application.

The example from our Handlebars chapter showed how to handle an action on a route:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
actions: {
notify: function(message){
window.alert("Template says: "+message);
}
}
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'<button {{action "notify" "clicked!"}}>Alert me</button>'
);

The template's target for actions is configured by the router, but the router doesn't set only the route as a handler for actions. Instead, a heirarchy of objects exist that each have the opportunity to handle this action. Ember calls this behavior "bubbling," a term taken from how DOM nodes handle events.

By default, an action will bubble through: 

  1. The controller backing this template.
  2. Any parent controllers (and their parents).
  3. The current leaf route (the most specific route you are on).
  4. The parent routes.
  5. Finally, the application route.

The "notify" action from our example above will do the same. It is handled by the IndexRoute via these steps:

  1. The generated IndexController. This does not handle the action, so it continues to bubble.
  2. IndexController having no parents, the IndexRoute will next receive the action. Here the action is handled.

If the IndexRoute did not handle the action, then the ApplicationRoute would have an opportunity to handle it. If the ApplicationRoute did not then handle the action, an error would be raised.

Parent Controllers and Bubbling

A controller-creating helper will always provide a parent controller. In this way, {{#each and {{render provide new places to handle actions. For instance, given an action firing from within a rendered template, the action could be handled on the controller of the render helper, the route-rendered template, or the route.

Here is the last example we saw, but with a modification to fire notify from within a rendered template:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
actions: {
notify: function(){
window.alert("IndexRoute handled the event.");
}
}
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from index</button>' +
'{{render "rendered"}}'
);
Ember.TEMPLATES['rendered'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from rendered</button>'
);

The button in the "rendered" template can be intercepted by handling the action on the RenderedController:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
actions: {
notify: function(){
window.alert("IndexRoute handled the event.");
}
}
});
App.RenderedController = Ember.Controller.extend({
actions: {
notify: function(){
window.alert("Ahha I've stolen the action!");
}
}
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from index</button>' +
'{{render "rendered"}}'
);
Ember.TEMPLATES['rendered'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from rendered</button>'
);

Or, both actions could be intercepted on the IndexController:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
actions: {
notify: function(){
window.alert("IndexRoute handled the event.");
}
}
});
App.IndexController = Ember.Controller.extend({
actions: {
notify: function(){
window.alert("Ahha I've stolen BOTH actions!");
}
}
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from index</button>' +
'{{render "rendered"}}'
);
Ember.TEMPLATES['rendered'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from rendered</button>'
);

But then RenderedController could steal back the action from only its own template:


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
actions: {
notify: function(){
window.alert("IndexRoute handled the event.");
}
}
});
App.IndexController = Ember.Controller.extend({
actions: {
notify: function(){
window.alert("Ahha I've stolen BOTH actions!");
}
}
});
App.RenderedController = Ember.Controller.extend({
actions: {
notify: function(){
window.alert("No way, stole my button back!");
}
}
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from index</button>' +
'{{render "rendered"}}'
);
Ember.TEMPLATES['rendered'] = Ember.Handlebars.compile(
'<button {{action "notify"}}>Alert from rendered</button>'
);

By default, an action is handled only once. To cause an action to keep bubbling even though it was handled, returns true from the handler.

The item controller created by{{#eachbehave in the same manner. Action bubbling is the cornerstone of an application's architecture, deciding where the responsibility to perform an action lies. In chapter 11, you will get a practical introduction to actions in the section "Editing Tickets."

Communicating Between Controllers

Not all state is local to a single part of an application. To address this, there are several ways one controller can access another. Combined with two-way data binding, they make shared state between parts of an application simple.

parentController

Often, rendered lists must access state, like a toggle, or data on a parent. The item controller of an {{#each helper is provided access to its parent. In this example, an observer updates all item controllers to match the toggle state of the parent. In this way they can change their own value, or be toggled by the parent.


var App = Ember.Application.create();
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.IndexController = Ember.ObjectController.extend({
allChecked: false
});
App.ColorController = Ember.Controller.extend({
isChecked: false,
updateWithMainToggle: function(){
this.set('isChecked', this.get('parentController.allChecked'));
}.observes('parentController.allChecked')
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'{{input type="checkbox" checked=allChecked}} Toggle all' +
'<ul>{{#each model itemController="color"}}' +
'<li>{{input type="checkbox" checked=isChecked}} Toggle {{model}}</li>' +
'{{/each}}</ul>'
);

Needs

Controllers created by calling {{render without a model, or instantiated by the router, can be pulled into other controllers via the needs property. An example:


var App = Ember.Application.create();
App.IndexController = Ember.Controller.extend({
needs: ['session'],
user: Ember.computed.alias('controllers.session.currentUser')
});
App.SessionController = Ember.Controller.extend({
currentUser: 'Carol'
});
Ember.TEMPLATES['index'] = Ember.Handlebars.compile(
'{{user}}' // => Carol
);

In this example, the IndexController is given access to the SessionController via the controllers.session property. SessionController is instantiated and treated as a singleton, so every controller that needs it will receive the same instance. Furthermore, this works with controllers wired to templates. As long as the controller is unattached, wired up by the router, or created by the {{render helper without a model, its instance will be the same as that returned by needs.

needs is an important tool in Ember, but can easily be abused. When controllers are dependent on many other controllers, an application becomes difficult to test and change. Always consider the tradeoffs inherent in coupling controllers to each other.

Good alternatives to needs include use of controllerFor on routes to access controller instances and send messages, and the register/inject dependency injection of Ember containers.

Wrapping Up

Controllers are an extraordinarily powerful part of Ember applications. Their flexibility can make abuse easy to stumble into. By staying focused on the responsibilities of presentation and state management, you can keep controllers from becoming un-focused and overly complex.

In the next chapter, we will explore components and learn how they combine the responsibilities of views and controllers into a single class. In some places where you may imagine {{render is the best solution, a component could be a better and more re-usable tool.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.