Question

Hi, I'm about 2 weeks into building my application with ember.js, and the time has come to pull together my project layout into its final shape. To that end, I started looking into using Ember's register / inject mechanism instead of just creating global singletons and attaching them to my App object (for an excellent description of dependency injection in Ember, see here)

I'm stuck with standard dependency injection dilemma - circular references.

Let's say, I have two manager-like classes that I need available throughout application. Let's call them AuthManager and DataManager.

App.AuthManager = Ember.Object.extend({
    logIn: function (user) {
        var promise = this.dataManager.post("/session/new", user);
        //...
    }
});

App.DataManager = Ember.Object.extend({
    getJSON: function (url) {
        if (!this.authManager.get("isLoggedIn")) {
            return false;
        }
        //...
    }
});

So, as you see dataManager needs access to authManager and vice-versa. My naive take at a solution was something like this:

App.initializer({
    name: "dataManager",

    initialize: function (container, application) {
        application.register("my:dataManager", application.DataManager);
        application.inject("my:authManager", "dataManager", "my:dataManager");
    }
});

App.initializer({
    name: "authManager",

    initialize: function (container, application) {
        application.register("my:authManager", application.AuthManager);
        application.inject("my:dataManager", "authManager", "my:authManager");
    }
});

Predictably, this results in a dead loop. I was hoping dependency injection system would try some crafty juggle, like node's require does, but no.

I have tried:

  • Moving the first inject into the second initializer, after my:authManager is registered.
  • Moving the first inject into its own initializer, after the first two
  • Putting any combination of these into the Ember.onLoad('Ember.Application', ...) from the linked article

Unfortunately, everything I tried ended in stack overflow (pun intended :-)).

Am I missing something? The documentation is pretty sparse in this area. Of course, I can always manually lookup instance after the 'official' injection, but I was hoping for some more elegant solution.

Was it helpful?

Solution

You definitely have a circular dependency, and if you were using a different language I'd tell you to use the inversion of control pattern, but it's a little difficult using your problem and the container.

Solution 1

If you're fine adding them under a namespace such as manager or something like that then here's a solution (it's tightly coupled, but the code is tightly coupled already, almost enough that they could be together or a mixin on the other).

App.Manager = Ember.Object.extend({
  init: function(){
    // late fake injection
    this.authManager.dataManager = this.dataManager;
    this.dataManager.authManager = this.authManager;
  }
});


App.initializer({
    name: "manager",
    after:['dataManager', 'authManager'],

    initialize: function (container, application) {
        application.register("my:manager", application.Manager);
        application.inject("my:manager", "dataManager", "my:dataManager");
        application.inject("my:manager", "authManager", "my:authManager");
        application.inject("controller", "manager", "my:manager");
        application.inject("route", "manager", "my:manager");
    }
});

App.initializer({
    name: "dataManager",

    initialize: function (container, application) {
        application.register("my:dataManager", application.DataManager);
    }
});

App.initializer({
    name: "authManager",

    initialize: function (container, application) {
        application.register("my:authManager", application.AuthManager);
    }
});

And an example:

http://emberjs.jsbin.com/mopaquko/2/edit

Solution 2

On another note, this creates a new instance on each route/controller. If you only need one instance. You can do it like so, much easier and doesn't need the namespace.

App.initializer({
    name: "joinManagers",
    after:['dataManager', 'authManager'],

    initialize: function (container, application) {
      var dataManager = container.lookup('my:dataManager'),
          authManager = container.lookup('my:authManager');
        
        authManager.dataManager = dataManager;
        dataManager.authManager = authManager;
      
        application.register("my:jointDataManager", dataManager, {instantiate:false});
        application.register("my:jointAuthManager", authManager, {instantiate:false});
        application.inject("controller", "dataManager", "my:jointDataManager");
        application.inject("controller", "authManager", "my:jointAuthManager");
        application.inject("route", "dataManager", "my:jointDataManager");
        application.inject("route", "authManager", "my:jointAuthManager");
    }
});


App.initializer({
    name: "dataManager",

    initialize: function (container, application) {
        application.register("my:dataManager", application.DataManager);
    }
});

App.initializer({
    name: "authManager",

    initialize: function (container, application) {
        application.register("my:authManager", application.AuthManager);
    }
});

http://emberjs.jsbin.com/mopaquko/3/edit

Solution 3

As was pointed out, Ember's container create's singletons by default, you can eagerly create the copies then allow ember to still resolve based on the original namespace.

App.initializer({
    name: "joinManagers",
    after:['dataManager', 'authManager'],

    initialize: function (container, application) {
      var dataManager = container.lookup('my:dataManager'),
          authManager = container.lookup('my:authManager');

        authManager.dataManager = dataManager;
        dataManager.authManager = authManager;

        application.inject("controller", "dataManager", "my:dataManager");
        application.inject("controller", "authManager", "my:authManager");
        application.inject("route", "dataManager", "my:dataManager");
        application.inject("route", "authManager", "my:authManager");
    }
});


App.initializer({
    name: "dataManager",

    initialize: function (container, application) {
        application.register("my:dataManager", application.DataManager);
    }
});

App.initializer({
    name: "authManager",

    initialize: function (container, application) {
        application.register("my:authManager", application.AuthManager);
    }
});

http://emberjs.jsbin.com/mopaquko/7/edit

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top