5

Transclusion, Injection and Procrastination

 3 years ago
source link: https://urish.medium.com/transclusion-injection-and-procrastination-8e1581c7a34e
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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:

Image for post
Image for post
Twitter, where the fun stuff is happening!

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:

Image for post
Image for post

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?

Image for post
Image for post
They used to call it Transclusion. Now it is called “Content Projection”. Photo by ng-conf

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!

Image for post
Image for post
I transcluded the Chrome T-Rex in this blog post

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:

And if you are attending AngularConnect, you are invited to come and say hi!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK