Understanding Dependency Injection in Ember

in Development

One of the essential concepts in Ember is dependency injection. Have you ever wondered why in routes or controllers you have access to store, but it’s not available in components? Or maybe you were trying to access an object like this.container.lookup('service:store') or and weren’t sure why these objects are registered under certain namespaces. And what exactly does Ember.inject.service() do under the hood? So today, let’s take a look at how dependency injection works and how it makes it all possible.

Dependency Injection — Why Bother?

Dependency injection (DI) is a design pattern that makes objects no longer responsible for instantiating any required collaborating objects, but instead the collaborating objects are passed to these objects from outside. Not only does it make the code less coupled and more extensible, but the testing is also much easier — nothing is hardcoded, which makes it easy to stub the dependencies out. Let’s see then how exactly Ember applies this pattern and how we can benefit from it.

Dependency Injection in Ember — Register & Inject

Imagine that in a component, you want to display some error message when things go wrong (not saying it’s the best UX possible, but it will work as an example). One way would be to directly use the alert function, and the problem would be solved — but it makes it much more difficult to test such logic in unit tests. Wouldn’t it be nice if we could just swap it with some other dependency when we need to? Thanks to the Ember DI mechanism, it’s pretty simple to achieve.

Let’s assume the API for using this error handler with alert will be the following:

import Ember from 'ember';

export default Ember.Component.extend({
  // perform some action here
  this.get('error-handler').display('Something went wrong');
});

To make it possible, we need two things: some object that will be compatible with this interface, and a way to have it available under error-handler. We can achieve all this with an initializer. In Ember apps, initializers live under the app/initializers directory. Let’s generate one:

ember g initializer error-handler

Here’s how the content of the generated file should look:

// app/initializers/error-handler.js

export function initialize(application) {

}

export default {
  name: 'error-handler',
  initialize
};

Nothing fancy so far — just an empty function taking the application argument. Let’s start with defining the ErrorHandler object:

// app/initializers/error-handler.js

import Ember from 'ember';

let ErrorHandler = Ember.Object.extend({
  display(message) {
    alert(message)
  }
});

ErrorHandler is just an Ember object that somehow needs to be later instantiated and available in our component. We will use the application argument for that, which exposes the register function that makes it possible to register anything under certain namespace. The expected format for the namespace is type:name — for example, serializer:user. In our case, let’s call it error-handler:main, which seems to be the most common convention.

// app/initializers/error-handler.js

import Ember from 'ember';

let ErrorHandler = Ember.Object.extend({
  display(message) {
    alert(message)
  }
});

export function initialize(application) {
  application.register('error-handler:main', ErrorHandler);
}

export default {
  name: 'error-handler',
  initialize
};

With this single line of code, we’ve successfully registered our error handler factory! It’s not yet available under this.get('error-handler') in our application’s components, but you can verify that it works by executing the following code inside a component:

Ember.getOwner(this).lookup('error-handler:main').display('Hey, it works!')

If you’re wondering what the Ember.getOwner function is, it’s the new API introduced since Ember 2.3 in favor of accessing the container directly, which returns “owner” of objects — Ember.ApplicationInstance. You can use the lookup function for fetching any registered objects under a given namespace, like ‘adapter:article’ or 'error-handler:main’ in our case. The interesting thing is what we really have registered under 'error-handler:main’ was the factory (or class) — not an instance of it. Turns out that by default, Ember instantiates all registered objects. In this case, that behavior makes sense, but if we implemented the error handler as a simple function, we would need to make sure instantiate option is set to false:

// app/initializers/error-handler.js

import Ember from 'ember';

let errorHandler = function(message) {
  alert(message);
}

export function initialize(application) {
  application.register('error-handler:main', errorHandler, { instantiate: false });
}

export default {
  name: 'error-handler',
  initialize
};

The important fact is that by default, every registered object is treated as a singleton — it will be instantiated only once, and for every other lookup, the cached instance will be returned. For such an error handler, it doesn’t make much difference, but if you maintain some kind of state (e.g., a use case-specific counter) inside your object, it certainly does matter. If you need to change the default behavior, you can set the singleton option to false:

// app/initializers/counter.js

// some Counter factory implementation here

export function initialize(application) {
  application.register('counter:main', Counter, { singleton: false });
}

We still need to provide some better API for accessing the error handler. We will achieve this using the inject function, also inside the initializer:

// app/initializers/error-handler.js
import Ember from 'ember';

let ErrorHandler = Ember.Object.extend({
  display(message) {
    alert(message)
  }
});

export function initialize(application) {
  application.register('error-handler:main', ErrorHandler);
  application.inject('component', 'error-handler', 'error-handler:main');
}

export default {
  name: 'error-handler',
  initialize
};

The inject function takes three arguments. The first is a factory type where we want to inject some object, function, or factory; the second is the name of the property under which a given object will be available; and the third is a namespace for the registered object that will be injected. In this case, the error-handler will be available in all components, but we can also inject it solely to a specific component:

application.inject('component:my-awesome-component', 'error-handler', 'error-handler:main');

And that’s it! You can use this error handler just like it was supposed to be used:

this.get('error-handler').display('Something went wrong');

Making Injection Even Simpler

Ember DI isn’t too complex once you understand it, but creating initializers and making injections every time we add some service seems like unnecessary boilerplate code. Fortunately, it can be done in a much simpler way. If we created ErrorHandler as an Ember.Service (instead of Ember.Object) in app/services directory, it would be automatically registered under the service:error-handler namespace during the booting process of the app. Let’s create such a service:

// app/services/error-handler.js

import Ember from 'ember';

export default Ember.Service.extend({
  display(message) {
    alert(message)
  }
});

We could already access the instance of the service by:

Ember.getOwner(this).lookup('service:error-handler');

But it’s not a convenient API. Fortunately, Ember offers a great shortcut for exactly this kind of use case: the Ember.inject.service() function (and its equivalent for controllers: Ember.inject.controller()). Inside the component (or any Ember object), you’d specify the attribute under which the service is supposed to be available:

import Ember from 'ember';

export default Ember.Component.extend({
  handler: Ember.inject.service('error-handler')
});

And the error handler will be available as handler property:

this.get('handler');

There’s also an even bigger shortcut: If the name of the property is going to be the same as the name of the service (keeping in mind the the dasherized property name will be used for the service lookup), you don’t have to provide the name of the service at all:

import Ember from 'ember';

export default Ember.Component.extend({
  errorHandler: Ember.inject.service()
});

The error handler will be available under the errorHandler property:

this.get('errorHandler');

If you’re wondering how it works under the hood, you may want to take a look at the source code for injected property and injection helper, but basically it comes down to owner.lookup('service:name') and the InjectedProperty API.

Wrapping Up

Dependency injection is one of the core ideas to understand when developing any meaningful Ember application. We’ve gone through initializers and registration/the injection API, which is more than enough to grasp this concept and enjoy yet another great abstraction layer provided by the framework. Let us know any tips or tricks you have in the comments below or refresh your knowledge of Ember with our Ember course!

Code School

Code School teaches web technologies in the comfort of your browser with video lessons, coding challenges, and screencasts. We strive to help you learn by doing.

Visit codeschool.com

About the Author

Karol Galanciak

Karol Galanciak

Karol is a technical polyglot who enjoys difficult refactoring challenges, API and architecture design in Rails, and building beautiful frontend apps. Karol is also an avid bodybuilder and guitar addict, loves learning new things, and loves staying productive.

Might We Suggest

Try Ember Course

Pack your bags and help the Woodland Wanderers as you learn how to use Ember and Ember CLI to build your next ambitious web applications.

View Course