Where do I put my "globals"?


#1

Apps have global (to the page) data. Some apps have very little – maybe just a currentUser. Others, like ours, have a lot – currentUser, currentAccount, userPermissions, locales, timeZones, voiceSettings, …

Where do you put this data?

We have come up with a few different options. Each has pros and cons.

Directly on the App

Assigning the current user object to App.currentUser makes it very easy to get to. Any controller can have something like

FooController = Em.Object.extend({
  isEnabledBinding: Em.Binding.oneWay('App.currentUser.canDoFoo')
});

We’ve found three major problems with this. First, it makes that controller difficult to test. We need to change the global in our setup and replace it with the original in our teardown (so we don’t affect other tests). Second, bindings to globals like this leak memory, though that problem may be specific to Ember 0.9. Last – and this is related to the testing difficulties – it’s generally fragile; if we want to delay fetching the currentUser, many other things may have to change.

Directly on the App, Local Aliases

We can solve the testability problem by making a local alias to globals:

FooController = Em.Object.extend({
  currentUser: Em.Binding.oneWay('App.currentUser'),
  isEnabledBinding: Em.Binding.oneWay('currentUser.canDoFoo')
});

This is better, but (a) we still have the binding to the global, which (at least on 0.9) leaks memory; and (b) we’re still reaching out for our dependencies instead of having them injected.

“instance” Properties

We can avoid binding to the global by moving the singleton to a class property:

User = Em.Object.extend({
  ...
});
User.current = User.create({...})

FooController = Em.Object.extend({
  currentUser: User.current,
  isEnabledBinding: Em.Binding.oneWay('currentUser.canDoFoo')
});

We’re still reaching out to dependencies, though.

Injecting through Routes

We could move all of these globals to the app’s IndexRoute or a more specific route if the globals aren’t needed in all sections of the app:

App.IndexRoute = Ember.Route.extend({
  currentUser: App.User.create(),
  
  setupController: function(controller) {
    controller.set('currentUser', this.get('currentUser');
  }
});

Now we’re getting somewhere (in theory)! The definition of the globals is centralized. Better yet, the globals are defined on an instance level, which means they won’t get automatically instantiated when loading the app for unit tests.

Unfortunately, I can’t get this to work very automatically in nested routes. I end up calling into IndexRoute#setupController manually from my nested routes.


#2

I’ve been experimenting with using initializers to inject a global current object into controllers and routes via the container:

App.initializer({
  name: 'injectCurrent',
  initialize: function(container, app) {
    container.optionsForType('globals', {instantiate: false, singleton: true});
    container.register('globals:current', Em.Object.create());

    container.typeInjection('controller', 'current', 'globals:current');
    container.typeInjection('route', 'current', 'globals:current');
  }
})

All routes and controllers instantiated will have access to current and you can set things on there to be available everywhere.

FooController = Em.Object.exted({
  isEnabledBinding: Em.Binding.oneWay('current.user.isEnabled')
});

#3

In our app we have a CurrentUserController. When other controller’s require access to the current user, they specify needs: ['currentUser'] which gives them access to the CurrentUser controller singleton via the controllers.currentUser property. This pattern has worked well for us and makes things very easy to test.

In your case, it might make sense to have a SessionController which provides access to appropriate user/account/permissions/etc. The same pattern would apply.


#4

It also looks like once we get up to Ember 1.0, we could use the Container to lazily compute singletons. I think there are a few decent options in Ember 1.0. I wonder what patterns will emerge.


#5

We’re currently registering controllers as singletons. I’m still sussing out our config story, but that’s another ball of wax.

And just so people know what we’re talking about, this is how you register a SessionController as a singleton:

App.register('controller:session', App.SessionController, { singleton: true });