Angular's `@Host` decorator not reaching the t

2020-06-18 09:50发布

问题:

In my main app.ts I've declared a global provider :

providers: [{provide: Dependency, useValue: createDependency('AppModule provider')}]

(Where createDependency is just a function that returns a class which has a getName() method.)

I also have a components :

    <my-app-component-3>Hello from 3</my-app-component-3>

Code :

@Component({
    selector: 'my-app-component-3',
    template: `
        <div>Component3:
            <ng-content></ng-content>
            : <span [innerHTML]="dependency?.getName()"></span>
        </div>
    `,

})
export class Component3 {
    constructor(@Host() @Optional() public dependency: Dependency) {}
}

The result is:

Component3: Hello from 3 :

But I expect the result to be :

Component3: Hello from 3 :AppModule provider

Because basically the app structure is :

<my-app>
  <my-app-component-3>
  </my-app-component-3>
</my-app> 

Question:
Why doesn't @Host() match the parent provider ?

(which is : providers: [{provide: Dependency, useValue: createDependency('AppModule provider')}])

To my knowledge - the injector should seek for a Dependency in this manner :

So why doesn't it find it ?

PLUNKER

Notice

I already know that if I remove @host - it does reach the top. My question is why adding @host - is not reaching the top - despite the fact thatmy-component3 is under my-app !!

回答1:

Check out A curios case of the @Host decorator and Element Injectors in Angular for in-depth explanation of how @Host decorator works and where Element Injectors come into this picture.

In order for it to work you should define dependencies in the in the parent component and using viewProviders:

@Component({
  selector: 'my-app',
  viewProviders: [{provide: Dependency, useValue: createDependency('AppModule provider')}],
    ...
export class MyApp {}

Here is what the comments inside metadata.ts say:

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

So basically it says that a host element injector and all injectors above are not used when resolving a dependency. So if your MyApp component has the following template:

<my-app-component-3></my-app-component-3>

and the resulting components tree look like this:

<my-app>
    <my-app-component-3></my-app-component-3>
</my-app>

neither MyApp component's injector nor App module injectors are used to resolve dependency for the my-app-component-3.

However, there's the following interesting code in the ProviderElementContext._getDependency that performs one additional check:

// 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;
    }
}

which basically checks if the provider is defined in the viewProviders and resolves it if found. That's why viewProviders work.

So, here is the lookup tree:

Usage

This decorator is mostly used for directives to resolve providers from the parent injector within the current component view. Even the unit test is written only to test directives. Here is a real example from the forms module how it's decorator is used.

Consider this template for the A component:

<form name="b">
    <input NgModel>
</form>

NgModel directive wants to resolve a provider supplied by the form directive. But if the provider is not available, there's no need to go outside of a current component A.

So NgModel is defined like this:

export class NgModel {
    constructor(@Optional() @Host() parent: ControlContainer...)

While form directive is defined like this:

@Directive({
  selector: '[formGroup]',
  providers: [{ provide: ControlContainer, useExisting: FormGroupDirective }],
  ...
})
export class NgForm

Also, a directive can inject dependencies defined by its hosting component if they are defined with viewProviders. For example, if MyApp component is defined like this:

@Component({
    selector: 'my-app',
    viewProviders: [Dependency],
    template: `<div provider-dir></div>`
})
export class AppComponent {}

the Dependency will be resolved.



回答2:

I wonder if the @Optional() is injecting null. I believe that one might be the culprit.

Edit

So from your plunker I can’t seem to find an actual host for the component 3. Something like

   <parent-component>
       <component-3><component-3/>
<parent-component/>

On my understanding here it seems what it’s looking for.



回答3:

just remove @Host() decorator from your Component 3 constructor:

Component({
    selector: 'my-app-component-3',
    template: `
        <div>Component3:
            <ng-content></ng-content>
            : <span [innerHTML]="dependency?.getName()"></span></div>
    `,

})
export class Component3 {
    constructor(@Optional() public dependency: Dependency) {}
}

Angular will take the provider from the AppModule.



回答4:

straight from Angular's docs on dependency injection and the Host decorator: https://angular.io/guide/dependency-injection-in-action#qualify-dependency-lookup-with-optional-and-host

The @Host decorator stops the upward search at the host component.

The host component is typically the component requesting the dependency.

with the @Host decorator, you're telling it to only check the host component for a provider, and you're making it optional, so it's just seeing there's no provider and quitting.

In practice, the use case for the Host decorator is extremely narrow, and really only ever makes sense if you're projecting content.