Transclusion, Injection and Procrastination
Exploring The Hidden Corners of Angular Dependency Injection
A few months ago, Kapunahele Wong and I were brainstorming ideas for talk proposals we could do together. It was around the time I decided to explore Ivy, so I thought that could be a good fit for a talk. Kapunahele, however, had a different idea:
This was the first time I heard about Injector Trees, so I was intrigued. Kapunahele shared with me some of her preliminary research, and I learned that the dependency resolution algorithm of the Angular Injector was not as simple as I thought before.
In fact, since NgModules were introduced in the 5th release candidate of Angular 2.0.0, I was only providing services in the module level, using the familiar providers property:
Or since Angular 6, using the providedIn property of the Injectable decorator:
Either way, I always declared my services at the module-level, and never paid to much attention to the other possibilities.
Enter Injector Trees 🌴
Kapunahele and I decided to submit our Injector Trees talk to Angular Connect. Several months later, the following message landed in my inbox:
We were pumped, our talk was accepted for the conference! 😊
We spent the next few weeks exploring all the hidden corners of how dependency injection works in Angular. In this blog post, I am going to share some of them with you.
Starting Simple
We will start with a simple Angular app. This app has a “Kingdom” service and a single component that injects Kingdom and displays the name of the Kingdom:
I decided to go with a Dragon for the example, as I love using them instead of semicolons in my code.
To make things a bit more interesting, let’s spice our app with a Unicorn component. This component prints the name of the Kingdom where it lives:
So we have an app component, and a unicorn inside it. Great!
Now, what happens when we change the definition of our AppComponent
to provide a different value for the KingdomService
?
We can do this by adding the following line to the component declaration:
providers: [ { provide: KingdomService, useValue: { name: '🧟' } }]
How will this affect our app? Let’s try it and see:
As you can see, the value we defined for KingdomService
in our AppComponent
took precedence over the service defined in our AppModule (it wasn’t directly defined there, rather using providedIn
, but the result is the same).
Element Tree, Module Tree
The reason we see zombies is the way dependency resolution works in Angular. It first searches the tree of components, and only then the tree of modules. Let’s consider UnicornComponent
. It injects an instance of KingdomService
inside its constructor:
constructor(public kingdom: KingdomService) {}
When Angular creates this component, it first looks if there are any providers defined on the same element as the component. These providers could have been registered on the component itself, or using a directive. In this case, we didn’t provide any value for KingdomService
inside the UnicornComponent
, nor do we have any directives on the <app-unicorn>
element.
The search then continues up the element tree, going to AppComponent
. Here Angular finds that we have a value for KingdomService
, so it injects this value and stops searching there. So in this case, Angular didn’t even look at the tree of modules.
Angular is Lazy!
Just like us programmers, Angular is also a procrastinator. It doesn’t create instances of services unless it really needs to. You can confirm this by adding a console.log
statement to the constructor of KingdomService
(you can also add an alert('I love marquees')
if you feel nostalgic today).
You will see that the console.log
statement is never executed — as Angular does not create the service. If you remove the providers:
declaration from the AppComponent
(or move it to the UnicornComponent
, so it only applies to the unicorn and its child elements), you should start seeing the log message in your console.
Now Angular has no choice — it doesn’t find the KingdomService
when looking in the Element Tree. So, first it goes to the Module Injector Tree, then sees that we provided the service there, and finally creates an instance of it. Hence, the code inside the constructor runs, and you will be able to see the debug print you put in there.
Directive Invasion!
I mentioned that directives can also provide values for dependency injection. Let’s experiment with that. We are going to define a new appInvader
directive, that will change the value of the kingdom to 👾.
Why? Because they were so lovely in the VR + Angular talk Alex Castillo and I gave in ng-conf.
Then, we will add another <app-unicorn>
element, and apply the new appInvader
directive to it:
As expected, the new unicorn lives in the 👾’s kingdom. This is because the directive provided a value for KingdomService
. And as explained above, Angular starts the search from the current element, looking at the Component and all the Directives, and only if it can’t find the requested value there, it continues going up the element tree (and then the modules).
Let’s look at something a bit more complicated:
Adding a Content-Projecting Forest to the App!
We will add a new Forest component to our app, and put some of the unicorns inside this forest, because that’s were unicorns live (some random guy said that on quora, so it must be true).
The Forest component is simply a container, which uses Content Projection to display its children on top of a green “foresty“ background:
So we see the elements of AppForest
component on a grass background, and then, all the projected content on top of a bright green background. And since we provided a value for KingdomService
inside our app component, everything inside inherits it (except for the one unicorn with the appInvader
directive).
But what happens if we provide a new value for KingdomService
inside the ForestComponent
? Will the projected content (that was defined in the template for AppComponent
) also get this new value for the kingdom? Or will it still be in the 🧟 kingdom? Can you guess?
The Wizard of The Forest
We will add a single line to our previous example, providing a 🧙 kingdom for the ForestComponent
:
providers: [ { provide: KingdomService, useValue: { name: '🧙' } }]
And this is the result:
Now this is interesting — we see a mixture of kingdoms inside the forest! The forest element itself lives in the 🧙 kingdom, but the projected content seems to have split personality: the unicorns also belong to the 🧙 kingdom, but the text above them shows 🧟 kingdom?
We defined both these unicorns and the text in the same place, lines 12–15 of the app.component.html
template. However, what matters is the place where the component itself was created in the DOM. The text in line 12 is actually the same as what we do it line 4 — we read the kingdom
property of the same AppComponent
instance. The DOM element for this component is actually an ancestor of the <app-forest>
DOM element. So when this AppComponent
instance was created, it was injected with the 🧟 kingdom.
The two <app-unicorn>
elements, are, however, inside the <app-forest>
DOM elements, so when their instances of UnicornComponents
are created, angular actually goes up the DOM and sees the value we provided for the KingdomService
inside the ForestComponent
, and thus these unicorns are injected with the 🧙 kingdom.
You can achieve a different behavior if you change providers
to viewProviders
when defining the ForestComponent
. You can learn more about View Providers here, and also check out this code example, where I changed ForestComponent
to use View Providers, so now even the unicorns inside the forest are injected with the 🧟 kingdom. Thanks Lars Gyrup Brink Nielsen for pointing this out to me!
Keep Exploring!
I hope that you have just learned something new about Angular’s Dependency Injection system. This is just one of the things Kapunahele and I explored when we prepared our talk for AngularConnect. There is much more — you are invited to explore further, and we will also share the slides and the link to the video after the talk. Oh, and there will be some live coding too. It’s gonna be a lot of fun!
If you wish to learn more about the ins and outs of the Angular Injector, here are a few articles I found very helpful:
- What you always wanted to know about Angular Dependency Injection
- A curious case of the @Host decorator and Element Injectors in Angular
- Hierarchical Dependency Injectors (Angular Docs)
And if you are attending AngularConnect, you are invited to come and say hi!
Thanks 🦄, 🧟, 🐉, 🧙 for participating in this blog post. 👾 wasn’t invited, obviously.
This is the 25th post in my Postober Challenge — writing something new every single day throughout October.
I will tweet whenever I publish a new post, promise! ✍