Should we use controllers?

Controllers are going away and components & services are taking over. This is part of an effort to improve the framework architecture (and lessen the cognitive struggle?) in Ember.

Despite the good news, Ember is in state of flux and controllers are still a concept we must be familiar with. They are labeled as thing of the past yet are a crucial building block today.

This “mixed message” is causing frustration.

What’s the place of controllers in a “services and components” world? Best practices going forward?

First things first: What is a controller?

A controller is a routable object meant to “decorate” a model with display logic. Example:

export default Ember.Controller.extend({
  emphasizeName() {
    return `HEY ${this.get('model.firstName')} !!!`
  },
  isVisible: Ember.computed.and('model.firstName', 'model.lastName')  
})

You wouldn’t put emphasizeName in your model, would you? This is clearly a display logic concern.

{{#if isVisible}}
  <span>{{ emphasizeName }} Kill the controller!</span>
{{/if}}

Every template in our applications is backed by a controller. Every property in a template is a property of its controller.

Deprecated confusion

If a controller handles display logic, this is where you’d handle events and interact with the DOM, correct?

Wrong. The infamous View was used for that purpose. Views have been removed from Ember 2.0, henceforth all that kind of interaction happens in components.

ArrayController and ObjectController (two decrepit classes) were used ages ago to proxy the model, such that controller.firstName would delegate to model.firstName. Modern-day controllers (extending Ember.Controller) do not, so you have to explicitly call model.firstName.

While there were obviously good intentions behind these ideas, they essentially contributed to blur concepts and cause confusion.

Coming back to our example, what does “routable” mean and how did that model end up in the controller?

That’s right, a route loads the model and then calls the setupController method, that by default is:

export default Ember.Route.extend({

  model() {
    // returns a model
  },

  setupController(controller, model) {
    controller.set('model', model);
  }

})

Not only do controllers hold template state. They are singletons and, as such, are conveniently used to keep longer lived state (like sessions that persist across requests). Were used, I mean.

The obsolete needs API allowed us to require any other controller and access its properties, for example this.get('controllers.position.progress').

We now use Services singletons to store long-lived state, which are injectable via Ember.inject.service().

Says the latest official guide:

Controllers are very much like components, so much so that in future versions of Ember, controllers will be replaced entirely with components.

Ember best practices delivered straight to your inbox? Tell me where:

(One e-mail every month. No BS. Unsubscribe anytime!)

What is a component?

It’s true that controllers are like components. In a way. Components are isolated by design and are fully responsible for a portion of the UI.

In other words, it has the functions of a template (with its own template), a controller (in that it holds state “backing” that template) and a view (event handling and DOM-related interaction).

Should we still use controllers?

The latest Ember guides state that “modern Ember applications don’t often use controllers” and that “controllers will be replaced entirely with components”.

Let’s listen to what Ember creators Tom and Yehuda have to say about this subject:


The idea then is to minimize controller usage. They are still needed mainly for two reasons:

The day routable components land in Ember, we will be able to route a URL/model directly to a component. Until then, we can employ this pattern:

This temporary shim layer is used as a glue between a route and a “top-level” component. Once a model has reached that top-level component, it will eventually propagate the model throughout its child component hierarchy.

Translation example

For most migrations, renaming export default Ember.Controller.extend(...) to export default Ember.Component.extend(...) will just work, as components partly have the role of controllers.

How about an example to illustrate this better?

Before

App.AlbumController = Ember.ObjectController.extend({

  needs: ['audio'],

  equalizer: function() {
    var genre = this.get('genre');
    return this.get('controllers.audio').equalizerFor(genre);
  }.observes('knob'),

  // ...

});

After

This could be roughly translated to newer Ember versions as:

{{!-- app/template/album.hbs --}}

{{ album-knobs model=model }}
// app/components/album-knobs.js

export default Ember.Component.extend({

  audio: Ember.inject.service(),

  equalizer() {
    const genre = this.get('model.genre');
    return this.get('audio').equalizerFor(genre);
  }.observes('knob')

});

// app/services/audio.js

export default Ember.Service.extend({
  equalizerFor(genre) {
    // ...
  }
});

We:

(This is a super simple example I just made up!)

Wrapping up, the conclusion is: minimize the usage of controllers. Use them only for the bare minimum: to forward a model to a “top-level” component (as “shim layer”) and/or for query params. When routable components arrive, it will be a matter of a few adjustments.

I hope this post helps you strengthen the bridge between “old” and “new” Ember. Any questions? Anything I can improve? Let me know in the comments!

Enjoyed this article? Don't miss my next one!

Leave me your e-mail for content that will help you master Ember:

Do you want to master Ember fast?

Leave me your e-mail for helpful updates delivered straight to your inbox.

(A few e-mails per month. No BS. Unsubscribe anytime!)