Communication Between Controllers in Ember.js
Wow! This takes me back! Please check the date this post was authored, as it may no longer be relevant in a modern context.
Ember.js maps templates to a view and controller- and first time users often start off trying to understand how they relate to each other.
Easier to grasp is the way templates relate. They organize themselves in a hierarchy by the use of render
, template
, partial
, view
, and outlet
handlebars helpers. The router moves between different states of this hierarchy.
This local message passing and routing can take you pretty far with a CRUD app, but eventually most apps need to pass messages laterally between edges of this hierarchy. There are a few ways to do this in Ember. If you have a new app, you’ll find these useful techniques. If you have an older Ember app you might be using outdated methods for achieving the same thing.
The Controller Hierarchy
Controllers in Ember shadow the same tree that templates are in. Messages propagate that tree until they hit the top controller for that route. Given these templates:
<script type="text/x-handlebars" id="index">
<hr>
<h2>Index</h2>
<p>Last action: {{lastAction}}</p>
<div>{{render 'wheels'}}</div>
</script>
<script type="text/x-handlebars" id="wheels">
<hr>
<h3>Wheels</h3>
<p>Last action: {{lastAction}}</p>
<button {{action 'rotateWheels'}}>Rotate</button>
</script>
The action from the wheels template, {{action 'rotateWheels'}}
will propagate up the tree until it stops at the IndexRoute
events object. You could handle this event on the WheelsController
, or even further up the tree at the IndexController
.
var App = Em.Application.create();
App.IndexController = Em.Controller.extend({
rotateWheels: function(){
this.set('lastAction', 'rotationOfWheels');
}
});
The IndexController
will handle the rotateWheels
action, despite that action being called form the wheels
view. Messages can be explicitly passed up the stack from a controller.
var App = Em.Application.create();
App.IndexController = Em.Controller.extend({
rotateWheels: function(){
this.set('lastAction', 'rotationOfWheels');
}
});
App.WheelsController = Em.Controller.extend({
rotateWheels: function(){
this.set('lastAction', 'rotation');
// target is the parent controller or the route.
this.get('target').send('rotateWheels');
}
});
Now both WheelsController
and IndexController
will handle the message rotateWheels
. IndexRoute
is the route for this page, so the message could be handled there as well.
var App = Em.Application.create();
App.IndexRoute = Em.Route.extend({
events: {
rotateWheels: function(){
this.controllerFor('index').set('lastAction', 'rotationOfWheels');
this.controllerFor('wheels').set('lastAction', 'rotate');
}
}
});
controllerFor
is used to fetch singleton instances of a given controller. If no controller handles the message and the route does not handle the message, an error will be raised. The error for this example message would be Uncaught Error: Nothing handled the event 'rotateWheels'.
Arguments can be passed via send
or via action
, though I’ve only passed the message name itself in these examples.
Note that message bubbling stops at the leaf route. If you have a nest route, the message will only bubble to that route, then it will raise the Uncaught
error. The message will not propagate through parent routes or their templates’ controllers.
Declaring Dependencies with needs
Sometimes, controllers must communicate with their sibling controllers. For this, needs
is a useful tool.
App.WheelsController = Em.Controller.extend({
needs: ['navigation', 'index']
});
at JSBin - The handlebars templates on the JSBin will be helpful.
WheelsController
will now have a property of controllers.navigation
returning an instance of it’s neighboring singleton controller. It also has access to controllers.index
which is the parent IndexController
singleton.
Declaring Dependencies with register
A more aggressive way to specify dependencies is with register
and inject
.
var App = Em.Application.create({
ready: function(){
this.register('session:current', App.Session, {singleton: true});
this.inject('controller', 'session', 'session:current');
}
});
App.Session = Em.Object.extend();
This sets the session
property of every controller instance to a singleton instance of App.Session
. register
can register controllers, models, views, or an arbitrary type like session
as above. inject
, in turn, can inject onto all instances of a given class, or all instances of a given type. inject('model', 'session', 'session:current')
injects a session
property with the session:current
instance onto all models. inject('view:index', 'session', 'session:current')
injects a session
property with the session:current
instance onto instances of the IndexView
.
register
and inject
are very powerful tools, and easy to abuse. I usually use them as a tool of last resort- or for logic that does not fit smoothly into the Ember MVC pattern.