Ember gotcha: Controllers are singletons

26 June 2014

There is a somewhat subtle bug in the current version of the Rock & Roll with Ember app. If you start to create a song for an artist that does not have one and then switch to another one, the song creation process does not need to be restarted by clicking on the "Why don't you create one?" link. Also, if you've already partially inputted the name of the song for the first one, it stays there for the second artist:

Start creating a song for Radiohead Input Karma for Radiohead Observe song title stays for Red Hot Chili Peppers

The above behavior was brought to my attention by David Lormor, an astute reader and watcher of my screencasts.

In some circumstances it is desirable for the result of some user interaction to linger between route transitions but in other cases it is not. I put the current example firmly in the latter camp and thus consider the above a bug.

Let me explain what causes this behavior and then provide a simple solution to fix it.

Repeat after me: controllers are singletons

Controllers in Ember are singletons. Controllers in Ember are singletons. Controllers in Ember are singletons.

When the user leaves a page and goes to another one, the controller is not torn down. It lives on, keeping its properties.

This makes total sense for a framework that aims to be a tool for creating long-lived, rich-client side applications but is something to watch out for when you develop Ember applications.

If you have a long background in back-end development, like yours truly, it is especially easy to fall prey to this, as you could see.

Same controller, different model

Initially, when the applicaiton is loaded, the songCreationStarted property of the controller is set to false. When the user clicks the "create one" in the "Why don't you create one?" blurb, it is set to true and thus the text input field appears to allow adding a new song.

Now comes the tricky part. If the user then decides to go to a different artist, she clicks the name of another artist in the sidebar. What happens? A transition is made from one ArtistSongsRoute to another ArtistSongsRoute. The artist is going to be different but the same ArtistSongsController is used.

To prove my point, here are two screenshots of the Ember Inspector's sidebar. The first one is before, the second one is after the transition between /artists/radiohead/songs and /artists/red-hot-chili-peppers/songs:

Ember Inspector - Radiohead songs Ember Inspector - RHCP songs

You can see that the controller is the same Ember object but the value of the artist property changes.

Understanding the problem

When the transition is made between the two artists, the artist object is changed and consequently any data bound to the artist (and the artist property of the ArtistSongs controller) is going to be rerendered but, since the controller instance is not changed, unrelated data will stay unchanged on screen.

What happens in the code? If you take a look at the template, you see that the text input field is shown if canCreateSong is true:

 1<script type="text/x-handlebars" data-template-name="artist/songs">
 2  {{#if canCreateSong}}
 3    <div class="list-group-item">
 4      {{input type="text" class="new-artist" placeholder=newSongPlaceholder value=newTitle insert-newline="createSong" }}
 5      <button class="btn btn-primary btn-sm new-song-button" {{action "createSong"}}>Add</button>
 6      (...)
 7    </div>
 8  {{/if}}
 9</script>

canCreateSong is defined in the controller and is true if either songCreationStarted is true or if there are already songs. Since songCreationStarted has just been set to true by the enabledSongCreation action (and the controller instance is not changed) when the user clicked the "create one" link for the first artist, canCreateSong stays true and the text field stays visible:

 1App.ArtistSongsController = Ember.ArrayController.extend({
 2  (...)
 3  songCreationStarted: false,
 4  canCreateSong: function() {
 5    return this.get('songCreationStarted') || this.get('length');
 6  }.property('songCreationStarted', 'length'),
 7
 8  actions: {
 9    enableSongCreation: function() {
10      this.set('songCreationStarted', true);
11    }
12  }
13  (...)
14});

A simple fix

To fix this, the simplest solution is to manually reset the properties that we do not want to persist.

One solution is to do this in the setupController of the route since that always gets called when transitioning to a new route:

 1App.ArtistSongsRoute = Ember.Route.extend({
 2  (...)
 3  setupController: function(controller, model) {
 4    this._super(controller, model);
 5    controller.set('artist', this.modelFor('artist'));
 6    controller.set('newTitle', '');
 7    controller.set('songCreationStarted', false);
 8  },
 9  (...)
10});

And that totally works. However, I prefer placing this "resetting" code in the controller, probably because it is strictly controller-related and also because I can make the code really telling there:

 1App.ArtistSongsController = Ember.ArrayController.extend({
 2  (...)
 3  songCreationStarted: false,
 4  canCreateSong: function() {
 5    return this.get('songCreationStarted') || this.get('length');
 6  }.property('songCreationStarted', 'length'),
 7
 8  artistDidChange: function() {
 9    this.set('newTitle', '');
10    this.set('songCreationStarted', false);
11  }.observes('artist'),
12  (...)
13});

The observes function property extension will run the artistDidChange code every time the artist property of the controller changes and that is exactly when we need to clear the title and allow the song creation process to be restarted. Nice, clean and does exactly one thing.

Note: The latest version of the code that contains these changes is available on Github.

Model Dependent State - UPDATE

It turns out I stumbled into something substantial. As Luke Melia points out below in the comments (thank you, Luke!), the above problem has been under consideration for a while.

Alex Matchneer, a member of the Ember core team gave a presentation at EmberConf in which he explains that there is a missing primitive.

He calls it "Model Dependent State" and it is what would decide under what conditions a certain controller property is "sticky" (whether it retains its value when you change the model of a certain controller or not).

Here is the part where he begins to talk about Model Dependent State.

Share on Twitter