Essential Angular: Dependency Injection
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.
- Essential Angular. Part 1: Compilation
- Essential Angular. Part 2: NgModules
- Essential Angular. Part 3: Components and Directives
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.
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:
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:
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.
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.
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.