resolution modifiers are decorators applied to dependency declarations, and restrict the set of injectors from which the dependency can be resolved. as discussed in the previous article, the DI system consists of an injector tree, with the root injector at the top and any number of component and directive injectors below it. the search to resolve a dependency begins at the component or directive itself and works its way toward the root. resolution modifiers allow the start or end point of this search to be changed. we can talk about them creating a 'boundary' past which a dependency cannot be resolved. I personally don't believe it's particularly clear from the Angular docs where precisely each resolution modifier sets its boundaries. in this article, I will examine a number of cases and show where the boundary is. additionally, I will explain in what circumstances the different resolution modifiers could be useful
@Optional
we'll start with this modifier as it's the most straightforward one. it simply means that no error will be thrown if the annotated dependency is not resolved. unlike the other modifiers, this doesn't change the scope of the DI injector system
code
export class ChildComponent {
constructor(@Optional() private loggingService: LogService) {
...
}
}
usage
for implementing optional functionality: e.g. a logging system that only operates in development mode
@Self()
The self modifier prevents the DI system searching for a provider beyond the component element.
<app-parent @Provide(MyService=AlphaService)>
<#VIEW @Provide(MyService=BetaService)>
<!— Self barrier —>
<app-child>
<#VIEW>
@Inject(@Optional() @Self() MyService) => Null
</#VIEW>
</app-child>
</#VIEW>
</app-parent>
if a directive exists on the component element and has a provider configured on it, it will be searched by the DI system to try and resolve MyService. (In fact, if MyService exists in such a directive’s providers array, that would override one in the component’s)
<app-parent @Provide(MyService=AlphaService)>
<#VIEW @Provide(MyService=BetaService)>
<!— Self end-point —>
<app-child @Provide(MyService=GammaService) app-foo[@Provide(MyService=ZetaService)]>
<#VIEW>
@Inject(@Optional() @Self() MyService) => ZetaService
</#VIEW>
</app-child>
</#VIEW>
</app-parent>
@SkipSelf()
<app-parent @Provide(MyService=AlphaService)>
<#VIEW @Provide(MyService=BetaService)>
<!— SkipSelf start-point —>
<app-child @Provide(MyService=GammaService) app-foo[@Provide(MyService=ZetaService)]>
<#VIEW>
@Inject(@SkipSelf() MyService) => BetaService
</#VIEW>
</app-child>
</#VIEW>
</app-parent>
example the use of @SkipSelf would be if you were providing a service in the parent scope that you were overriding in the child scope, but for a particular dependency you wanted it to use the service in the parent scope. a good example is NgModelGroup in the Angular forms module. it provides a ControlContainer for its children, but also injects a ControlContainer from its parent. to prevent the injector returning the wrong instance, @SkipSelf is used so that the component injects the latter and not the former
@Host()
the @Host modifier restricts the provider scope to a component and the template that contains it.
in the following example, the child component selector appears in the parent template
<!-- parent.component.html -->
<app-child></app-child>
when a dependency in the child component is annotated with the Host modifier, the DI system cannot reach beyond the viewProviders of the parent component. even though ParentComponent provides the desired service, the dependency cannot be resolved
<app-parent @Provide(MyService=AlphaService)>
<!— Host end-point —>
<#VIEW>
<app-child>
<#VIEW>
@Inject(@Optional() @Host() MyService) => null
</#VIEW>
</app-child>
</#VIEW>
</app-parent>
enabling viewProviders on ParentComponent causes the service to be found there as that is within the barrier put up by the Host modifier
<app-parent @Provide(MyService=AlphaService)>
<!— Host end-point —>
<#VIEW @Provide(MyService=BetaService)>
<app-child>
<#VIEW>
@Inject(@Optional() @Host() MyService) => BetaService
</#VIEW>
</app-child>
</#VIEW>
</app-parent>
@Host with content projection
the app-child component selector is now declared within the AppComponent template
<!-- app.component.html -->
<app-parent>
<app-child></app-child>
</app-parent>
now, the viewProviders of AppComponent are in scope of the declared dependency whilst the providers of AppComponent are not
<app-component @Provide(MyService=GammaService)>
<!-- Host end-point -->
<#VIEW>
<app-parent>
<#CONTENT>
<app-child>
<#VIEW>
@Inject(@Optional() @Host() MyService) => null
</#VIEW>
</app-child>
</#CONTENT>
<#VIEW @Provide(MyService=BetaService)> // not reachable
</#VIEW>
</app-parent>
</#VIEW>
</app-component>
the 'Host' is the component of the template a selector is declared within. <app-child>
is within the app.component.html template, therefore its host is AppComponent. only AppComponent's viewProviders are in scope, not its providers. note the viewProviders of ParentComponent contain an instance of MyService, but this is not used since as the viewProviders are attached to the view, they are in a different branch of the provider tree and not accessible from the injection point
putting Host on a dependency means that its Host component has to provide it, and cannot rely on it being provided somewhere further up the provider tree. it is a way of insuring that a component is wrapped in one of the correct type.
conclusion
resolution modifiers create boundaries in the provider tree restricting the providers the DI system can search in to resolve dependencies. this is useful for limiting the scope of particular dependencies and providing guards that ensure that components and directives are used correctly. understanding where the boundaries are set for different modifiers is crucial for a proper understanding of Angular dependency injection