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');
  }
});

at JSBin

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');
  }
});

at JSBin

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');
    }
  }
});

at JSBin

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();

at JSBin

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.