In this section, we will take a look at how we can build a hierarchy of injectors. This is a completely new concept in the framework introduced by Angular 2. Each injector can have either zero or one parent injectors, and each parent injector can have zero or more children. In contrast to AngularJS where all the registered providers are stored in a flat structure, in Angular 2 and later versions, they are stored in a tree. The flat structure is more limited; for instance, it doesn't support the namespacing of tokens; we cannot declare different providers for the same token, which might be required in some cases. So far, we have looked at an example of an injector that doesn't have any children or a parent. Now, let's build a hierarchy of injectors.
In order to gain a better understanding of this hierarchical structure of injectors, let's take a look at the following diagram:
Figure 1
Here, we see a tree where each node is an injector, and each of these injectors keeps a reference to its parent. The injector House has three child injectors: Bathroom, Kitchen, and Garage.
Garage has two child injectors: Car and Storage. We can think of these injectors as containers with registered providers inside of them.
Let's suppose that we want to get the value of the provider associated with the token Tire. If we use the injector Car, this means that Angular's DI mechanism will try to find the provider associated with this token in Car and all of its parents, Garage and House, until it finds it.
In order to gain a better understanding of the paragraph, let's take a look at this simple example:
// ch5/ts/parent-child/simple-example.ts class Http {} @Injectable() class UserService { constructor(public http: Http) {} } let parentInjector = ReflectiveInjector.resolveAndCreate([ Http ]); let childInjector = parentInjector.resolveAndCreateChild([ UserService ]); // UserService { http: Http {} } console.log(childInjector.get(UserService)); // true console.log(childInjector.get(Http) === parentInjector.get(Http));
The imports are omitted since they are not essential to explain the code. We have two services, Http
and UserService
, where UserService
depends on the Http
service.
Initially, we create an injector using the resolveAndCreate
static method of the ReflectiveInjector
class. We pass an implicit provider to this injector, which will later be resolved to a provider with an Http
token. Using resolveAndCreateChild
, we resolve the passed providers and instantiate an injector, which points to parentInjector
(so, we get the same relation as the one between Garage and House shown in the previous diagram).
Now, using childInjector.get(UserService)
, we are able to get the value associated with the UserService
token. Similarly, using childInjector.get(Http)
and parentInjector.get(Http)
, we get the same value associated with the Http
token. This means that childInjector
asks its parent for the value associated with the requested token.
However, if we try to use parentInjector.get(UserService)
, we won't be able to get the value associated with the token, since its provider is registered in childInjector
.
Now that we're familiar with the injectors' hierarchy, let's see how we can get the dependencies from the appropriate injectors in it.
Now, let's suppose that we have the following configuration:
abstract class Channel {} class Http extends Channel {} class WebSocket extends Channel {} @Injectable() class UserService { constructor(public channel: Channel) {} } let parentInjector = ReflectiveInjector.resolveAndCreate([ { provide: Channel, useClass: Http } ]); let childInjector = parentInjector.resolveAndCreateChild([ { provide: Channel, useClass: WebSocket }, UserService ]);
We can instantiate the UserService
token using:
childInjector.get(UserService);
In UserService
, we can declare that we want to get the Channel
dependency from the current injector (that is, childInjector
) using the @Self
decorator:
@Injectable() class UserService { constructor(@Self() public channel: Channel) {} }
Although this will be the default behavior during the instantiation of the UserService
, using @Self
, we can be more explicit. Let's suppose that we change the configuration of childInjector
to the following:
let parentInjector = ReflectiveInjector.resolveAndCreate([ { provide: Channel, useClass: Http } ]); let childInjector = parentInjector.resolveAndCreateChild([ UserService ]);
If we keep the @Self
decorator in the UserService
constructor and try to instantiate UserService
using childInjector
, we will get a runtime error because of the missing provider for Channel
.
In some cases, we may want to use the provider registered in the parent injector instead of the one registered in the current injector. We can achieve this behavior by taking advantage of the @SkipSelf
decorator. For instance, let's suppose that we have the following definition of the Context
class:
class Context { constructor(public parentContext: Context) {} }
Each instance of the Context
class has a parent. Now, let's build a hierarchy of two injectors, which will allow us to create a context with a parent context:
let parentInjector = ReflectiveInjector.resolveAndCreate([ { provide: Context, useValue: new Context(null) } ]); let childInjector = parentInjector.resolveAndCreateChild([ Context ]);
Since the root context doesn't have a parent, we will set the value of its provider to be new Context(null)
.
If we want to instantiate the child context, we can use:
childInjector.get(Context);
For the instantiation of the child, Context
will be used by the provider registered within the childInjector
. However, as a dependency, it accepts an object that is an instance of the Context
class. Such classes exist in the same injector, which means that Angular will try to instantiate it, but it has a dependency of the Context
type. This process will lead to an infinite loop that will cause a runtime error.
In order to prevent it from happening, we can change the definition of Context
in the following way:
class Context { constructor(@SkipSelf() public parentContext: Context) {} }
The only change that we introduced is the addition of the parameter decorator @SkipSelf
.
Angular 2 introduced the @Optional
decorator, which allows us to deal with dependencies that don't have a registered provider associated with them. Let's suppose that a dependency of a provider is not available in any of the target injectors responsible for its instantiation. If we use the @Optional
decorator, during the instantiation of the dependent provider the value of the missing dependency will be passed null
.
Now, let's take a look at the following example:
abstract class SortingAlgorithm { abstract sort(collection: BaseCollection): BaseCollection; } @Injectable() class Collection extends BaseCollection { private sort: SortingAlgorithm; constructor(sort: SortingAlgorithm) { super(); this.sort = sort || this.getDefaultSort(); } } let injector = ReflectiveInjector.resolveAndCreate([ Collection ]);
In this case, we defined an abstract class called SortingAlgorithm
and a class called Collection
, which accepts an instance of a concrete class as a dependency that extends SortingAlgorithm
. Inside the Collection
constructor, we set the sort
instance property to the passed dependency of the SortingAlgorithm
type or a default sorting algorithm implementation.
We didn't define any providers for the SortingAlgorithm
token in the injector we configured. So, if we want to get an instance of the Collection
class using injector.get(Collection)
, we'll get a runtime error. This means that if we want to get an instance of the Collection
class using the DI mechanism of the framework, we must register a provider for the SortingAlgorithm
token, although we may want to fall back to a default sorting algorithm, returned by the getDefaultSort
method.
Angular provides a solution to this problem with the @Optional
decorator. This is how we can approach the problem using it:
// ch5/ts/decorators/optional.ts @Injectable() class Collection extends BaseCollection { private sort: SortingAlgorithm; constructor(@Optional() sort: SortingAlgorithm) { super(); this.sort = sort || this.getDefaultSort(); } }
In the preceding snippet, we declare the sort
dependency as optional, which means that if Angular doesn't find any provider for its token, it will pass the null
value.
Multiproviders are another new concept introduced to the DI mechanism of Angular in version 2. They allow us to associate multiple providers with the same token. This can be quite useful if we're developing a third-party library that comes with some default implementations of different services, but you want to allow the users to extend it with custom ones. They are also exclusively used to declare multiple validations over a single control in the Angular's form module. We will explain this module in Chapter 6, Working with the Angular Router and Forms, and Chapter 7, Explaining Pipes and Communicating with RESTful Services.
Another sample of an applicable use case of multiproviders is what Angular uses for event management in its Web Workers implementation. They create multiproviders for event management plugins. Each of the providers return a different strategy, which supports a different set of events (touch events, keyboard events, and so on). Once a given event occurs, they can choose the appropriate plugin that handles it.
Let's take a look at an example that illustrates a typical usage of multiproviders:
// ch5/ts/configuring-providers/multi-providers.ts const VALIDATOR = new OpaqueToken('validator'); interface EmployeeValidator { (person: Employee): string; } class Employee {...} let injector = ReflectiveInjector.resolveAndCreate([ { provide: VALIDATOR, multi: true, useValue: (person: Employee) => { if (!person.name) { return 'The name is required'; } } }, { provide: VALIDATOR, multi: true, useValue: (person: Employee) => { if (!person.name || person.name.length < 1) { return 'The name should be more than 1 symbol long'; } } }, Employee ]);
In the preceding snippet, we declare a constant called VALIDATOR
and as its value we set a new instance of OpaqueToken
. We also create an injector where we register three providers-two of them provide functions that, based on different criteria, validate instances of the class Employee
. These functions are of the type EmployeeValidator
.
In order to declare that we want the injector to pass all the registered validators to the constructor of the Employee
class, we need to use the following constructor definition:
class Employee { name: string; constructor(@Inject(VALIDATOR) private validators: EmployeeValidator[]) {} validate() { return this.validators .map(v => v(this)) .filter(value => !!value); } }
In the example, we declare a class Employee
that accepts a single dependency: an array of EmployeeValidator
s. In the method validate
, we apply the individual validators over the current class instance and filter the results in order to get only the ones that have returned an error message.
Note that the constructor argument validators
is of the EmployeeValidator[]
type. Since we can't use the type "array of objects" as a token for a provider, because it is not a valid value in JavaScript and can't be used as a token, we will need to use the @Inject
parameter decorator.
In Chapter 4, Getting Started with Angular Components and Directives, when we developed our first Angular directive, we saw how we can take advantage of the DI mechanism to inject services into our UI-related components (that is, directives and components).
Let's take a quick look at what we did earlier, but from a DI perspective:
// ch4/ts/tooltip/app.ts // ... @Directive(...) export class Tooltip { @Input() saTooltip: string; constructor(private el: ElementRef, private overlay: Overlay) { this.overlay.attach(el.nativeElement); } // ... } @Component({ // ... providers: [Overlay] }) class App {}
Most of the code from the earlier implementation is omitted because it is not directly related to our current focus.
Note that the constructor of Tooltip
accepts two dependencies:
ElementRef
class.Overlay
class.The types of dependencies are the tokens associated with their providers and the corresponding values obtained from the providers will be injected with the DI mechanism of Angular.
Although the declaration of the dependencies of the Tooltip
class looks exactly the same as what we did in the previous sections, there's neither any explicit configuration nor any instantiation of an injector. In this case, Angular internally creates and configures the so called element injector. We'll explain it a little bit later, but before that, lets take a look at how we can configure the DI mechanism using NgModules.
We have already talked about NgModules in Chapter 2, The Building Blocks of an Angular Application and Chapter 4, Getting Started with Angular Components and Directives. We mentioned that they help us to divide our application into logical parts; we also discussed how to use NgModules' imports and exports. In this section, we'll look at a brief overview of how we can use them to configure the providers of our application.
Based on the providers declared in a given NgModule, Angular will instantiate an injector. This injector will manage all the providers that are listed in the providers
property of the object literal we pass to the @NgModule
decorator:
class Markdown {...} @Component(...) class MarkdownPanel {...} @Component(...) class App {...} @NgModule({ declarations: [App, MarkdownPanel], providers: [Markdown], imports: [BrowserModule], bootstrap: [App], }) class AppModule {} platformBrowserDynamic().bootstrapModule(AppModule);
In the preceding example, we declare a single provider for the Markdown
service. It will be available in all the components and directives listed in the declarations
array because the injectors used by the top-level components will get the one configured through the NgModule as their parent injector.
Now, let's suppose that our module imports another module that has a declaration of providers:
// ch4/ts/directives-ngmodules/app.ts // ... @NgModule({ declarations: [Button], exports: [Button], providers: [Markdown], }) class ButtonModule {} //... @NgModule({ declarations: [App, MarkdownPanel], imports: [BrowserModule, ButtonModule], bootstrap: [App], }) class AppModule {} platformBrowserDynamic().bootstrapModule(AppModule);
In the example, we have two modules – the AppModule
we previously saw and ButtonModule
. In the snippet AppModule
imports ButtonModule
, which means that all the exports of ButtonModule
will be available as declarations in AppModule
. On top of that, the providers declared in ButtonModule
will be merged with the providers of AppModule
. Based on all these providers, Angular will instantiate an injector and set it as a parent injector to the injector used by the bootstrap component - App
.
Now, let's discuss the element injector that each component and directive has associated to it.
Under the hood, Angular will create injectors for all the directives and components, and add a default set of providers to them. These are the so-called element injectors and are something the framework takes care of itself. The injectors associated with the components are called host injectors. One of the providers in each element injector is associated with the ElementRef
token; it will return a reference to the host element of the directive. However, where is the provider for the Overlay
class declared? Let's take a look at the implementation of the top-level component:
@Component({ // ... providers: [Overlay] }) class App {}
We configure the element injector for the App
component by declaring the providers
property inside the @Component
decorator. At this point, the registered providers will be visible by the directive or the component associated with the corresponding element injector and the component's entire component subtree, unless they are overridden somewhere in the hierarchy.
Having the declaration of all the providers in the same place might be quite inconvenient. For example, imagine we're developing a large-scale application that has hundreds of components depending on thousands of services. In this case, configuring all the providers in the root component is not a practical solution. There will be name collisions when two or more providers are associated with the same token. The configuration will be huge, and it will be hard to trace where the different dependencies need to be injected.
As we mentioned, Angular's @Directive
(and @Component
) decorator allows us to introduce directive-specific providers using the providers
property. Here is how we can approach this:
@Directive({ selector: '[saTooltip]', providers: [{ provide: Overlay, useClass: OverlayMock }] }) export class Tooltip { @Input() saTooltip: string; constructor(private el: ElementRef, private overlay: Overlay) { this.overlay.attach(el.nativeElement); } // ... } // ... platformBrowserDynamic().bootstrapModule(AppModule);
The preceding example overrides the provider for the Overlay
token in the Tooltip
directive's declaration. This way, Angular will inject an instance of OverlayMock
instead of Overlay
during the instantiation of the tooltip.
Since components are generally directives with views, everything we've seen so far regarding how the DI mechanism works with directives is valid for components as well. However, because of the extra features that the components provide, we're allowed to have further control over their providers.
As we said, the injector associated with each component will be marked as a host injector. There's a parameter decorator called @Host
, which allows us to retrieve a given dependency from any injector until it reaches the closest host injector. This means that, using the @Host
decorator in a directive, we can declare that we want to retrieve the given dependency from the current injector or any parent injector until we reach the injector of the closest parent component.
The viewProviders
property added to the @Component
decorator is in charge of achieving even more control.
Let's take a look at an example of a component called MarkdownPanel
. This component will be used in the following way:
<markdown-panel> <panel-title># Title</pane-title> <panel-content> # Content of the panel * First point * Second point </panel-content> </markdown-panel>
The content of each section of the panel will be translated from markdown to HTML. We can delegate this functionality to a service called Markdown
:
import * as markdown from 'markdown'; class Markdown { toHTML(md) { return markdown.toHTML(md); } }
The Markdown
service wraps the markdown
module in order to make it injectable through the DI mechanism.
Now let's implement MarkdownPanel
.
In the following snippet, we can find all the important details from the component's implementation:
// ch5/ts/directives/app.ts @Component({ selector: 'markdown-panel', viewProviders: [Markdown], styles: [...], template: ` <div class="panel"> <div class="panel-title"> <ng-content select="panel-title"></ng-content> </div> <div class="panel-content"> <ng-content select="panel-content"></ng-content> </div> </div>` }) class MarkdownPanel { constructor(private el: ElementRef, private md: Markdown) {} ngAfterContentInit() { let el = this.el.nativeElement; let title = el.querySelector('panel-title'); let content = el.querySelector('panel-content'); title.innerHTML = this.md.toHTML(title.innerHTML); content.innerHTML = this.md.toHTML(content.innerHTML); } }
In the @Component
decorator, we use the markdown-panel
selector and set the viewProviders
property. In this case, there's only a single view provider: the one for the Markdown
service. By setting this property, we declare that all the providers declared in it will be accessible from the component itself and all of its view children.
Now, let's suppose we have a component called MarkdownButton
and we want to add it to our template in the following way:
<markdown-panel> <panel-title>### Small title</panel-title> <panel-content> Some code </panel-content> <markdown-button>*Click to toggle*</markdown-button> </markdown-panel>
The Markdown
service will not be accessible by the MarkdownButton
used below the panel-content
element; however, it'll be accessible if we use the button in the component's template:
@Component({ selector: 'markdown-panel', viewProviders: [Markdown], styles: [...], template: ` <div class="panel"> <markdown-button>*Click to toggle*</markdown-button> <div class="panel-title"> <ng-content select="panel-title"></ng-content> </div> <div class="panel-content"> <ng-content select="panel-content"></ng-content> </div> </div>` })
If we need the provider to be visible in all the content and view children, all we should do is change the name of the property viewProviders
to providers
.
You can find this example in the examples directory at ch5/ts/directives/app.ts
.
Note that, for any component or directive, we can override an existing provider declared in a NgModule using the providers
properties of the object literal we pass to the @Component
or the @Directive
decorators. If we want to override a specific provider only for the view children of a given component, we can use viewProviders
.
We are already proficient in using the DI of Angular with TypeScript! As we know, we are not limited to TypeScript for the development of Angular applications; we can also use ES5, ES2015, and ES2016 (as well as Dart, but that is outside the scope of this book).
So far, we declared the dependencies of the different classes in their constructor using standard TypeScript type annotations. All such classes are supposed to be decorated with the @Injectable
decorator. Unfortunately, some of the other languages supported by Angular miss a few of these features. In the following table, we can see that ES5 doesn't support type annotations, classes, and decorators:
ES5 |
ES2015 |
ES2016 | |
Classes |
No |
Yes |
Yes |
Decorators |
No |
No |
Yes (no parameter decorators) |
Type annotations |
No |
No |
No |
How can we take advantage of the DI mechanism in these languages? Angular provides an internal JavaScript domain-specific language (DSL), which allows us to take advantage of the entire functionalities of the framework using ES5.
Now, let's translate the MarkdownPanel
example we took a look at in the previous section from TypeScript to ES5. First, let's start with the Markdown
service:
// ch5/es5/simple-example/app.js var Markdown = ng.core.Class({ constructor: function () {}, toHTML: function (md) { return markdown.toHTML(md); } });
We defined a variable called Markdown
and set its value to the returned result from the invocation of ng.core.Class
. This construct allows us to emulate ES2015 classes using ES5. The argument of the ng.core.Class
method is an object literal, which must have the definition of a constructor
function. As a result, ng.core.Class
will return a JavaScript constructor function with the body of constructor
from the object literal. All the other methods defined within the boundaries of the passed parameter will be added to the function's prototype.
One problem is solved: we can now emulate classes in ES5; there are two more problems left!
Now, let's take a look at how we can define the MarkdownPanel
component:
// ch5/es5/simple-example/app.js var MarkdownPanel = ng.core.Component({ selector: 'markdown-panel', viewProviders: [Markdown], styles: [...], template: '...' }) .Class({ constructor: [Markdown, ng.core.ElementRef, function (md, el) { this.md = md; this.el = el; }], ngAfterContentInit: function () { ... } });
From
Chapter 4, Getting Started with Angular Components and Directives, we are already familiar with the ES5 syntax used to define components. Now, let's take a look at the constructor function of MarkdownPanel
, in order to check how we can declare the dependencies of our components and classes in general.
From the preceding snippet, we should notice that the value of the constructor is not a function this time, but an array instead. This might seem familiar to you from AngularJS, where we are able to declare the dependencies of the given service by listing their names:
Module.service('UserMapper', ['User', '$http', function (User, $http) { // ... }]);
Although the new syntax looks similar, it brings some improvements. For instance, we're no longer limited to using strings for the dependencies' tokens.
Now, let's suppose we want to make the Markdown
service an optional dependency. In this case, we can approach this by passing an array of decorators:
... .Class({ constructor: [[ng.core.Optional(), Markdown], ng.core.ElementRef, function (md, el) { this.md = md; this.el = el; }], ngAfterContentInit: function () { ... } }); ...
This way, by nesting arrays, we can apply a sequence of decorators: [[ng.core.Optional(), ng.core.Self(), Markdown], ...]
. In this example, the @Optional
and @Self
decorators will add the associated metadata to the class in the specified order.
Although using ES5 makes our build simpler and allows us to skip the intermediate step of transpilation, which can be tempting, Google's recommendation is to take advantage of static typing using TypeScript. This way, we have a much clearer syntax, which carries better semantics with less typing and provides us with great tooling, including the straightforward process of AoT compilation (we'll explore the Angular's AoT in the final chapter of the book).