A curious case of the @Host decorator and Element Injectors in Angular

Post Editor

This article dives deep into mechanics of @Host decorator in Angular. It introduces rarely seen notion of Element Injector and shows how it's used alongside the decorator to retrieve dependencies.

8 min read
0 comments
post

A curious case of the @Host decorator and Element Injectors in Angular

This article dives deep into mechanics of @Host decorator in Angular. It introduces rarely seen notion of Element Injector and shows how it's used alongside the decorator to retrieve dependencies.

post
post
8 min read
0 comments
0 comments

As you know, Angular’s dependency injection mechanism includes a bunch of decorators like @Optional and @Self which impact the way dependencies are resolved. And while most of them are pretty straightforward and self-explanatory, the @Host decorator has puzzled me for a long time. I haven’t found the docs that explain it, except for a comment in the sources:

Specifies that an injector should retrieve a dependency from any injector until reaching the host element of the current component.

Since most tutorials on the web mention module and component injectors in Angular, I figured it’s related to a component’s injectors hierarchy. My assumption was that this decorator can be applied in a child component to restrict resolution only to itself and a parent’s component injector. So I put together a small example to test this hypothesis:

<>Copy
@Component({ selector: 'my-app', template: `<a-comp></a-comp>`, providers: [MyAppService] }) export class AppComponent {} @Component({selector: 'a-comp', ...}) export class AComponent { constructor(@Host() s: MyAppService) {} }

Alas, it produced the No provider for MyAppServic error. Interestingly, if I remove the @Host decorator the MyAppService is resolved from the parent component as expected. So what’s going on here? To find out, I rolled up my sleeves and started investigating ?. Let me share with you what I’ve found.

At the risk of jumping ahead, I’ll tell you now that the word until in the definition I mentioned above is very important:

…retrieve a dependency from any injector until reaching the host element

It means that the @Host decorator only applies to the resolution process inside a component’s template and doesn’t even get to the host element. That’s why I got the error in my demo — Angular wouldn’t resolve a dependency from a host parent component.

So now we know that the @Host decorator can’t be used in child components to resolve providers from parent components. It means that this decorator’s resolution mechanism is not using a hierarchy of component injectors.

So what kind of injectors hierarchy does it use?

Well, as it turns Angular has a third type of injectors besides modules and components. It’s the element injectors hierarchy that is created by HTML elements and directives.

Element Injectors

Angular resolves dependencies in 3 stages starting with the hierarchy of element injectors and moving up to component injectors and then module injectors. If you’re interested to learn about the details of an entire resolution process I highly recommend checking out this in-depth article by Alexey Zuev.

The last two stages of the resolution process that goes upwards through module and component injectors should be familiar to you. Angular creates a hierarchy of module injectors when you lazy load a module. I’ve explained this process in details in my talk at NgConf and have written an article. The hierarchy of component injectors is created by nesting components in templates. Internally, component injectors are also referred to as View Injectors and we’ll see shortly why.

A hierarchy of element injectors, on the other hand, is a lesser-known feature of Angular’s DI system mostly because it’s not really documented anywhere. But exactly this kind of injectors is the first stage of the DI resolution process. And these injectors make up a hierarchy that is used to resolve dependencies decorated with the @Host decorator. So let’s take a look at this kind of injectors.

An element injector
Link to this section

As you probably know from my previous articles, Angular internally represents a component using a data structure commonly referred to as a View or a Component View. Actually, that is where the name View Injector that refers to Component Injector comes from. The main purpose of a view is to hold references to DOM nodes created for HTML elements specified in a component’s template. So, internally, each view consists of different kinds of view nodes. The most common type is element node that holds a reference to a corresponding DOM element. Here is a diagram that represents a relationship between a view and DOM:

Content imageContent image

Each view nodeis created using a node definition that holds metadatadescribing the node. For example, a type of a node, like element type used to hold DOM element references. This metadata is generated by a compilerbased on the component’s template and directives applied to each element. Here is a diagram that represents a relationship between a view node definition and its instance:

Content imageContent image

A node definition describing a node of element type has one interesting peculiarity.

In Angular, a node definition that describes an HTML element defines its own injector. In other words, an HTML element in a component’s template defines its own element injector. And this injector can populated with providers by applying one or more directives on the corresponding HTML element.

Let’s see an example.

Suppose you have a component’s template with one div element and two directives A and B applied to it:

<>Copy
@Component({ selector: 'my-app', template: `<div a b></div>` }) export class AppComponent {} @Directive({ selector: '[a]' }) export class ADirective {} @Directive({ selector: '[b]' }) export class BDirective {}

The definition that Angular creates for this template includes the following metadata for the div element:

<>Copy
const DivElementNodeDefinition = { element: { name: 'div', publicProviders: { ADirective: referenceToADirectiveProviderDefinition, BDirective: referenceToBDirectiveProviderDefinition } } }

As you can see, this node definition defines element.publicProviders property that acts as an injector with two providers ADirective and BDirective. These are actually directive class instances applied to the div element. And since they are provided by the same element injector, you can inject one directive instance into the other. Of course, they can’t cross inject each other because it would be impossible to instantiating one without instantiating the other.

So here is diagram that illustrates what we have now:

Content imageContent image

Notice that a host app-comp element is outside the AppComponentView because it belongs to a parent view.

Now what do you think will happen if the directive A declares a provider?

<>Copy
@Directive({ selector: '[a]', providers: [ADirService] }) export class ADirective {}

Well, as expected, that provider will be added to the element injector created by the div element:

<>Copy
const divElementNodeDefinition = { element: { name: `div`, publicProviders: { ADirService: referenceToADirServiceProviderDefinition, ADirective: referenceToADirectiveProviderDefinition } } }

Again, if we put it on a diagram, here is what we have now:

Content imageContent image

Hierarchy of element injectors
Link to this section

In the section above we had only one HTML element. Nested HTML elements make up a hierarchy of DOM elements and in Angular’s DI system these elements constitute a hierarchy of element injectors within a component’s view.

Let’s see an example.

Suppose you have a component’s template with one parent and one child div elements. Also, we have two directives A and B. The directive A is applied to the parent div and declares ADirService provider. The directive B is applied to the child div and doesn’t declare any providers.

Here is the code that demonstrates the setup:

<>Copy
@Component({ selector: 'my-app', template: ` <div a> <div b></div> </div> ` }) export class AppComponent {} @Directive({ selector: '[a]', providers: [ADirService] }) export class ADirective {} @Directive({ selector: '[b]' }) export class BDirective {}

If we now explore the definition that Angular creates for this template, we will find two nodes of type element that describe metadata for both div elements:

<>Copy
const viewDefinitionNodes = [ { // element definition for the parent div element: { name: `div`, publicProviders: { ADirective: referenceToADirectiveProviderDefinition, ADirService: referenceToADirServiceProviderDefinition, } } }, { // element definition for the child div element: { name: `div`, publicProviders: { BDirective: referenceToBDirectiveProviderDefinition } } } ]

As we discovered in the previous section, eachdiv element definition haspublicProvidersproperty that acts as a DI container. And since A directive applied to a parent div also defines ADirService provider, it’s added to the element injector of a parent div.

This nested HTML structure creates a hierarchy of element injectors.

Interestingly, a child component also creates an element injector that is part of element injectors hierarchy. For example, the following template:

<>Copy
<div adir> <a-comp></a-comp> </div>

where adir declares a provider creates a hierarchy of two element injectors — parent injector created on the div element and the child injector created on the a-comp element. And it’s not surprising because a component is mostly an HTML element with a component directive applied to it.

Creating element injectors
Link to this section

When Angular creates an element injector for a nested HTML element, it either inherits it from a parent’s element injector or directly assigns a parent’s element injector to the child node definition. A prototype based inheritance between element injectors is only created if directives applied to a child element declare providers. In other words, if an element injector on the child HTML element has providers, the injector should be inherited. Otherwise, there’s no need to create a separate injector for a child component and if needed the dependencies can be resolved directly from a parent’s injector.

Here is a diagram that demonstrates this behavior:

Content imageContent image

Resolution process
Link to this section

Setting up a hierarchy between element injectors inside a component’s view simplifies the resolution process in element injectors. Instead of coming up with its own implementation of injector’s traversal, Angular relies on the JavaScript’s mechanism of a property lookup in the prototype chain to resolve a dependency in one step:

<>Copy
elDef.element.publicProviders[tokenKey]

And because of the way JavaScript works, a key in the publicProviders object will be resolved either directly from a parent’s element injector or through the prototype chain.

@Host decorator

So why are we talking about element injectors and not the @Host decorator? It’s because what this decorator does is to simply restrict a lookup to element injectors within one view. During the regular DI resolution process, if a token can’t be resolved using element injectors inside a view, Angular traverses parent views and checks view/component injectors. If not found, then module injectors are traversed and checked. But when the @Host decorator is used, the process stops at the first stage of resolving a dependency in element injectors within one component view.

Examples

The @Host decorator is heavily used inside built-in form directives. For example, to inject a hosting form into the ngModel directive and register a form created by the directive with the form. This is typical markup for a template driven form:

<>Copy
<form> <input ngModel> </form>

Under the hood, the form element is matched by a selector of the NgForm directive that registers itself as a ControlContainer provider:

<>Copy
@Directive({ selector: 'form', providers: [ { provide: ControlContainer, useExisting: NgForm } ] }) export class NgForm {}

The ngModel directive, in turn, injects a parent form using the same ControlContainer token and uses it to register a control with the form:

<>Copy
@Directive({ selector: '[ngModel]', }) export class NgModel { constructor(@Optional() @Host() parent: ControlContainer) {} private _setUpControl(): void { ... this.parent.formDirective.addControl(this); } }

As you can see, it uses the @Host decorator to restrict the resolution process only to the current component’s template. In most cases it’s exactly the desired behavior, but sometimes in nested forms you need to inject a hosting form from a parent component. Our curios friend Alexey Zuev has found a way to do that and has written an article. Check it out.

The article I referenced above also mentions another interesting behavior. If I tweak a little bit the example I started this article with by registering MyAppService in the viewProviders instead of providers:

<>Copy
@Component({ selector: 'my-app', template: `<a-comp></a-comp>`, viewProviders: [MyAppService] }) export class AppComponent {} @Component({selector: 'a-comp', ...}) export class AComponent { constructor(@Host() s: MyAppService) {} }

it gets resolved and is successfully injected into the child component.

It works because Angular has an additional check for viewProviders on the parent component when resolving a dependency decorated with @Host:

<>Copy
// check @Host restriction if (!result) { if (!dep.isHost || this.viewContext.component.isHost || this.viewContext.component.type.reference === tokenReference(dep.token !) || // this line this.viewContext.viewProviders.get(tokenReference(dep.token !)) != null) { <------ result = dep; } else { result = dep.isOptional ? result = {isValue: true, value: null} : null; } }

Comments (0)

Be the first to leave a comment

Share

About the author

author_image

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

author_image

About the author

Max Koretskyi

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

About the author

author_image

Principal Engineer at kawa.ai.. Founder indepth.dev. Big fan of software engineering, Web Platform & JavaScript. Man of Science & Philosophy.

Featured articles