Sitemap

Nx Devtools

Smart, Fast and Extensible Build System with First-Class Monorepo Support

Essential Angular: Dependency Injection

6 min readDec 27, 2016

--

Press enter or click to view image in full size

Victor Savkin is a co-founder of nrwl.io, providing Angular consulting to enterprise teams. He was previously on the Angular core team at Google, and built the dependency injection, change detection, forms, and router modules.

This is the fourth post in the Essential Angular series, which aims to be a short, but at the same time, fairly complete overview of the key aspects of Angular. In this post I’ll cover dependency injection.

Read the Series

Even though it’s not required, I recommend to read the first two posts in the series before starting on this one.

You can also check out the Essential Angular book, which has extra content not available on this blog.

Example App

Throughout this series I use the same application in the examples. This application is a list of tech talks that you can filter, watch, and rate.

You can find the source code of the application here.

Dependency Injection

The idea behind dependency injection is very simple. If you have a component that depends on a service. You do not create that service yourself. Instead, you request one in the constructor, and the framework will provide you one. By doing so you can depend on interfaces rather than concrete types. This leads to more decoupled code, which enables testability, and other great things.

Press enter or click to view image in full size

Angular comes with a dependency injection system. To see how it can be used, let’s look at the following component, which renders a list of talks using the for directive:

Let’s mock up a simple service that will give us the data.

How can you use this service? One approach is to create an instance of this service in our component.

This is fine for a demo app, but not good for real applications. In a real application TalksAppBackend won’t just return an array of objects, it will make http requests to get the data. This means that the unit tests for this component will make real http requests — not a great idea. This problem is caused by the fact that you have coupled TalksCmp to TalksAppBackend and its new operator.

You can solve this problem by injecting an instance of TalksAppBackend into the constructor, so you can easily replace it in tests, like this:

This tells Angular that TalksCmp depend on TalksAppBackend. Now, you need to tell Angular how to create an instance of TalksAppBackend.

Registering Providers

To do that you need to register a provider, and there are two places where you can do it. One is in the component decorator.

And the other one is in the module decorator.

What is the difference and which one should you prefer?

Generally, I recommend to register providers at the module level when they do not depend on the DOM, components, or directives. And only UI-related providers that have to be scoped to a particular component should be registered at the component level. Since `TalksAppBackend` has nothing to do with the UI, register it at the module level.

Injector Tree

Now you know that the dependency injection configuration has two parts:

  • Registering providers: How and where an object should be created.
  • Injecting dependencies: What an object depends on.

And everything an object depends on (services, directives, and elements) is injected into its constructor. To make this work the framework builds a tree of injectors.

First, every DOM element with a component or a directive on it gets an injector. This injector contains the component instance, all the providers registered by the component, and a few “local” objects (e.g., the element).

Second, when bootstrapping an `NgModule`, Angular creates an injector using the module and the providers defined there.

So the injector tree of the application will look like this:

Press enter or click to view image in full size

Resolution

And this is how the dependency resolution algorithm works.

When resolving the backend dependency of TalksCmp, Angular will start with the injector of the talks component itself. Then, if it is unsuccessful, it will climb up to the injector of the app component, and, finally, will move up to the injector created from AppModule. That is why, for TalksAppBackend to be resolved, you need to register it at TalkCmp, AppCmp, or AppModule.

Lazy Loading

The setup gets more complex once you start using lazy-loading.

Lazy-loading a module is akin to bootstrapping a module in that it creates a new injector out of the module and plugs it into the injector tree. To see it in action, let’s update our application to load the talks module lazily.

With this change, the injector tree will look as follows:

Press enter or click to view image in full size

Getting Injector

You can use ngProbe to poke at an injector associated with an element on the page. You can also see an element’s injector when an exception is thrown.

Press enter or click to view image in full size

Right click on any of these objects to store them as a global variable, so you can interact with them in the console.

Visualizing Injector Tree

If you more of a visual person, use the Angular Augury chrome extension to inspect the component and injector trees.

Press enter or click to view image in full size

Advanced Topics

Controlling Visibility

You can be more specific where you want to get dependencies from. For instance, you can ask for another directive on the same element.

Or you can ask for a directive in the same template, i.e., you can only inject an ancestor directive from the same HTML file.

Finally, you can ask to skip the current element, which can be handy for decorating existing providers or building up tree-like structures.

Optional Dependencies

To mark a dependency as optional, use the Optional decorator.

More on Registering Providers

Passing a class into an array of providers is the same as using a provider with useClass, i.e., the two examples below are identical:

When useClass does not suffice, you can configure providers with useValue, useFactory, and useExisting.

As you can see above, we can use the @Inject decorator to configure dependencies when the type parameter does not match the provided token.

Aliasing

It’s common for components and services to alias themselves.

Now we can use both @Inject(ComponentReexportingItself) and @Inject(‘alias’) to inject this component.

Overrides

The providers of the imported modules are merged with the target module’s providers, left to right, i.e., if multiple imported modules define the same provider, the last one wins.

The example above will print ‘B’. If we change ModuleC to have its own ‘token’ provider, that one will be used, and the example will print ‘C’.

Let’s Recap

  • Dependency injection is a key component of Angular.
  • You can configure dependency injection at the component or module level.
  • Dependency injection allows us to depend on interfaces rather than concrete types.
  • This results in more decoupled code.
  • This improves testability.

Essential Angular Book

This article is based on the Essential Angular book, which you can find here https://leanpub.com/essential_angular. If you enjoyed the article, check out the book!

Victor Savkin is a co-founder of Nrwl — Enterprise Angular Consulting.

If you liked this, click the💚 below so other people will see this here on Medium. Follow @victorsavkin to read more about Angular.

--

--

Nx Devtools
Nx Devtools

Published in Nx Devtools

Smart, Fast and Extensible Build System with First-Class Monorepo Support

Victor Savkin
Victor Savkin

Written by Victor Savkin

Nrwlio co-founder, Xoogler, Xangular. Work on dev tools for TS/JS. @NxDevTools and Nx Cloud architect. Calligraphy and philosophy enthusiast. Stoic.

Responses (3)

And only UI-related providers that have to be scoped to a particular component should be registered at the component level

Are there any concrete examples of good use cases of your component level providers? I can’t think of a single one. I would love to know!

--

“ Lazy-loading a module is akin to bootstrapping a module in that it creates a new injector out of the module and plugs it into the injector tree”
Isn’t the same true for feature modules that are not lazy-loaded? Wouldn’t any module create a new node in the injector tree for DI to traverse?

--

renders a list of talks

Should be
*ngFor=”let t of talks”
rather than *ngFor="#t of talks”

--