Child injectors and visibility

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:

Child injectors and visibility

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.

Building a hierarchy of injectors

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.

Configuring dependencies

Now that we're familiar with the injectors' hierarchy, let's see how we can get the dependencies from the appropriate injectors in it.

Using the @Self decorator

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.

Skipping the self injector

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.

Having optional dependencies

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.

Using multiproviders

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 EmployeeValidators. 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.

Using DI with components and directives

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:

  • An instance of the ElementRef class.
  • An instance of the 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.

Configuring DI with NgModules

We have already talked about NgModules in Chapter 2The Building Blocks of an Angular Application and Chapter 4Getting 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.

Introducing the element injectors

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.

Declaring providers for the element injectors

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.

Exploring DI with components

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.

viewProviders versus providers

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

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.

Using Angular's DI with ES5

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.

Note

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).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset