How do viewProviders and @Host work together in Angular?

48 Views Asked by At

Given the following example on stackblitz

@Injectable()
export class LogService {
  sayHello() {
    console.log('Hello World !');
  }
}
@Component({
  selector: 'app-child',
  standalone: true,
  template: `
    <div>Child Component !</div>
  `,
})
export class ChildComponent {
  constructor(@Host() @SkipSelf() logService: LogService) {
    logService.sayHello(); // it prints 'Hello World !'
  }
}
@Component({
  selector: 'app-parent',
  imports: [ChildComponent],
  standalone: true,
  viewProviders: [LogService],
  template: `
    <h1>Parent Component</h1>
    <app-child></app-child>
  `,
})
export class ParentComponent {}

I don't understand why logService is correctly injected into ChildComponent despite having @Host() and @SkipSelf() decorators.

@Host documentation :

The @Host property decorator stops the upward search at the host component. The host component is typically the component requesting the dependency. However, when this component is projected into a parent component, that parent component becomes the host.

viewProviders documentation :

Defines the set of injectable objects that are visible to its view DOM children

There is no projected content in this example, so the host component is ChildComponent.

The search should be stopped at ChildComponent and also be skipped for this component , so how can the DI framework see the LogService declared in ParentComponent viewProviders ?

I presumed viewProviders to do the same as providing the service into ChildComponent providers, but @SkipSelf() should prevent from searching inside this component.

Replacing viewProviders by providers produces the expected error No provider for LogService found.

The documentation about viewProviders is very light, I don't understand how it really work behind the scene.

1

There are 1 best solutions below

0
Olivier Boissé On

I finally understood how DI resolution works, so I will answer my own question so it could be useful for others.

@SkipSelf skips the current component providers / viewProviders.

@Host stops the search when reaching the viewProviders of the host component (host component providers won't be reachable).
The host component is the one declaring the current component in its template.

From the example :

  • @SkipSelf skips ChildComponent providers and viewProviders.

  • @Host stops the search at ParentComponent viewProviders. The host component is ParentComponent because it declares <app-child></app-child> in its template.

So LogService is eventually resolved from ParentComponent viewProviders.

Extra Notes about viewProviders and providers differences

  1. viewProviders are resolved before providers
@Injectable()
export class LogService {
  constructor(public message: string) {}
}

@Component({
  selector: 'app-root',
  standalone: true,
  providers: [{
    provide: LogService,
    useValue: new LogService('Root providers'),
  }],
  viewProviders: [{
    provide: LogService,
    useValue: new LogService('Root viewProviders'),
  }],
  template: `{{logService.message}}`, 
  // print 'Root viewProviders'
})
export class RootComponent {
  constructor(public logService: LogService) {}
}
  1. viewProviders and providers dependency resolution differs when using content projection.

viewProviders are only visible to the components declared in the template view.

@Injectable()
export class LogProvidersService {
  constructor(public message: string) {}
}

@Injectable()
export class LogViewProvidersService {
  constructor(public message: string) {}
}
@Component({
  selector: 'app-root',
  standalone: true,
  imports: [ParentComponent, ChildComponent],
  providers: [{
    provide: LogProvidersService,
    useValue: new LogProvidersService('Root providers'),
  }],
  viewProviders: [{
    provide: LogViewProvidersService,
    useValue: new LogViewProvidersService('Root viewProviders'),
  }],
  template: `
    <app-parent>
      <app-child></app-child>
    </app-parent>
  `,
})
export class RootComponent {}
@Component({
  selector: 'app-parent',
  standalone: true,
  providers: [{
    provide: LogProvidersService,
    useValue: new LogProvidersService('Parent providers'),
  }],
  viewProviders: [{
    provide: LogViewProvidersService,
    useValue: new LogViewProvidersService('Parent viewProviders'),
  }],
  template: `<ng-content></ng-content>`,
})
export class ParentComponent {}
@Component({
  selector: 'app-child',
  standalone: true,
  template: `
    <p>{{logProvidersService.message}}</p>
    <p>{{logViewProvidersService.message}}</p>
  `,
  // print 'Parent providers'
  // print 'Root viewProviders'
})
export class ChildComponent {
  constructor(
    public logProvidersService: LogProvidersService,
    public logViewProvidersService: LogViewProvidersService
  ) {}
}

I created a stackblitz project in case you want to play with it.

To resolve viewProviders, you should ask yourself "In which template is declared app-children tag ?". The answer is RootComponent so it has access to its viewResolvers (along with those from its ancestor if any).

It doesn't have access to ParentComponent viewResolvers because content projection (through ng-content) is used, viewResolvers are not accessible to projected content.