dependency injection (DI) is a fundamental feature of Angular. it provides a way for the objects in an application to be wired together. it allows dependencies to be declared as interfaces whose implementations are supplied at runtime, and can even be swapped with different implementations. the benefits are: to decouple objects from their dependencies, to reduce object creation boilerplate, and to facilitate testing units in isolation
Angular's DI system consists of a tree of injectors which largely mirrors the component tree. when a component tries to inject a dependency, it searches in the injector closest to it. if it cannot find it there, it will search upwards through the tree until it finds it, else an error will be thrown (unless the dependency is labelled as optional). by default, the tree has only a single root injector, and this may be sufficient for many applications; however, additional injectors can be configured by both components and directives. a component can configure two injectors through its providers
and viewProviders
properties. a directive can configure just one injector with its providers
property
when working with injectors, it can be confusing to developers what the scope of each injector is. in this article, I'll explore this and provide a clear explanation of when and why you should configure a particular injector for a component or directive
there are four main cases to consider and I'll take each one in turn. for each case, I'll present the following information:
- template: the template or templates in which component and directives are declared
- code: relevant code examples
- injection order: the order in which injectors are searched for the requested dependency
- logical view: a representation of the component tree as seen from the point of view of the DI system. the syntax used is adapted from the Angular DI docs
Case 1: a single component with providers and viewProviders
let’s start with the simplest case: a component with provider and viewProvider injectors providing services sharing the same DI token
// ChildComponent.ts
providers: [{provide: MyService, useClass: AlphaService}],
viewProviders: [{provide: MyService, useClass: BetaService}],
template
<app-child></app-child>
injector order
ChildComponent:viewProviders > ChildComponent:providers
logical view
<app-child @Provide(MyService=AlphaService)>
<#VIEW @Provide(MyService=BetaService)>
@Inject(MyService) => BetaService
</#VIEW>
</app-child>
BetaService is used as it is provided by the injector within scope that is closest to the injection point
Example:
example 1 the component injects the service from the viewProvider injector. if that is commented out, AlphaService from the provider injector is used instead
Case 2: child component selector declared in parent component template
a parent component and a child component where the child is declared within the parent’s template
template
<!— AppComponent template —>
<app-parent></app-parent>
<!— ParentComponent template —>
<app-child></app-child>
injector order
ChildComponent:viewProviders > ChildComponent:providers > ParentComponent:viewProviders > ParentComponent:providers
logical view
<app-parent @Provide(MyService=AlphaService)>
<#VIEW @Provide(MyService=BetaService)>
<app-child @Provide(MyService=GammaService)>
<#VIEW @Provide(MyService=DeltaService)>
@Inject(MyService) => DeltaService
</#VIEW>
</app-child>
</#VIEW>
</app-parent>
at the injection point, providers and viewProviders of both the child and parent component are all in scope
example
Case 3: directives on component elements
directives can also add injectors to the injector tree. the @Directive decorator supports the providers property only. all the injectors on a single element are combined into a single injector. the services from directives override those in the component. if there are several directives configuring services, the priority amongst directives is determined by the order in which directives are declared in the module
template
<app-child app-foo></app-child>
injector order
FooDirective:providers > ChildComponent:providers
logical view
<app-child @Provide(MyService=AlphaService) app-foo[@Provide(MyService=BetaService)]>
<#VIEW>
@Inject(MyService) => BetaService
</#VIEW>
</app-child>
the provider injectors for both AppComponent and FooDirective are combined into one injector. where the same DI token appears in both injectors, the service from the directive provider injector takes priority. the viewProvider injector of ChildComponent is invisible to FooDirective
example
Case 4: content projection
content projection is when a component element is nested within another in the same template. the template for the child component is inserted into that of its parent at the point specified by the <ng-content>
element. the child component is still able to access the provider injector of its parent, but the parent’s viewProvider injector is hidden to it
template
<app-parent>
<app-child></app-child>
</app-parent>
injector order
ChildComponent:viewProviders > ChildComponent:providers > ParentComponent:providers
logical view
<app-parent @Provide(MyService=AlphaService)>
<#VIEW @Provide(MyService=BetaService)>
</#VIEW>
<#CONTENT>
<app-child @Provide(MyService=GammaService)>
<#VIEW @Provide(MyService=DeltaService)>
@Inject(MyService) => DeltaService
</#VIEW>
</app-child>
</#CONTENT>
</app-parent>
the content is in a different scope from that of the view, thus ChildComponent does not have access to ParentComponent’s viewProvider injector
example
example 4 if you delete providers and viewProviders from ChildComponent, the dependency is resolved from the providers injector of ParentComponent. the viewProviders injector is ignored
Conclusion
a component can configure up to two injectors with the providers and viewProviders properties. a directive can configure one injector with the providers property. these injectors allow different parts of an application to have their own instance of a particular service
a directive declared on a component element can provide services which override the provider injector of the component
viewProviders declare services that are only available for the component's view. directives on the component element and projected content does not have access to these. thus, providers configure publicly available services whilst viewProviders configures services that are private to the component
in the next article on the subject of DI, I am going to explore how resolution modifiers like @Self and @Host change this picture