How dependency injection works in Angular 2

As our applications grows and evolves, each one of our code entities will internally require instances of other objects, which are better known as dependencies in the world of software engineering. The action of passing such dependencies to the dependent client is known as injection, and it also entails the participation of another code entity, named the injector. The injector will take responsibility for instantiating and bootstrapping the required dependencies so they are ready for use from the very moment they are successfully injected in the client. This is very important since the client knows nothing about how to instantiate its own dependencies and is only aware of the interface they implement in order to use them.

Angular 2 features a top-notch dependency injection mechanism to ease the task of exposing required dependencies to any entity that might exist in an Angular 2 application, regardless of whether it is a component, a directive, a pipe, or any other custom service or provider object. In fact, as we will see later in this chapter, any entity can take advantage of dependency injection (usually referred to as DI) in an Angular 2 application. Before delving deeper into the subject, let's look at the problem that Angular's DI is trying to address.

Let's figure out we have a music player component that relies on a playlist object to broadcast music to its users:

import { Component } from '@angular/core';
import { Playlist } from './playlist';

@Component({
  selector: 'music-player',
  templateUrl: './music-player.component.html'
})
class MusicPlayerComponent {
  playlist: Playlist;

  constructor() {
    this.playlist = new Playlist();
  }
}

The Playlist type could be a generic class that returns in its API a random list of songs or whatever. That is not relevant now, since the only thing that matters is that our MusicPlayerComponent entity does need it to deliver its functionality. Unfortunately, the implementation above means that both types are tightly coupled, since the component instantiates the playlist within its own constructor. This prevents us from altering, overriding, or mocking up in a neat way the Playlist class if required. It also entails that a new Playlist object is created every time we instantiate a MusicPlayerComponent. This might be not desired in certain scenarios, especially if we expect a singleton to be used across the application and thus keep track of the playlist's state.

Dependency injection systems try to solve these issues by proposing several patterns, and the constructor injection pattern is the one enforced by Angular 2. The previous piece of code could be rethought like this:

@Component({
  selector: 'music-player',
  templateUrl: './music-player.component.html'
})
class MusicPlayerComponent {
  playlist: Playlist;

  constructor(playlist: Playlist) {
    this.playlist = playlist;
  }
}

Now, the Playlist is instantiated outside our component. On the other hand, the MusicPlayerComponent expects such an object to be already available before the component is instantiated so it can be injected through its constructor. This approach gives us the opportunity to override it or mock it up if we wish.

Basically, this is how dependency injection, and more specifically the constructor injection pattern, works. However, what has this to do with Angular 2? Does Angular's dependency injection machinery work by instantiating types by hand and injecting them through the constructor? Obviously not, mostly because we do not instantiate components by hand either (except when writing unit tests). Angular features its own dependency injection framework, which can be used as a standalone framework by other applications, by the way.

The framework offers an actual injector that can introspect the tokens used to annotate the parameters in the constructor and return a singleton instance of the type represented by each dependency, so we can use it straight away in the implementation of our class, as in the previous example. The injector ignores how to create an instance of each dependency, so it relies on the list of providers registered upon bootstrapping the application. Each one of those providers actually provides mappings over the types marked as application dependencies. Whenever an entity (let's say a component, a directive, or a service) defines a token in its constructor, the injector searches for a type matching that token in the pool of registered providers for that component. If no match is found, it will then delegate the search on the parent component's provider, and will keep conducting the provider's lookup upwards until a provider resolves with a matching type or the top component is reached. Should the provider lookup finish with no match, Angular 2 will throw an exception.

Note

The latter is not exactly true, since we can mark dependencies in the constructor with the @Optional parameter decorator, in which case Angular 2 will not throw any exception and the dependency parameter will be injected as null if no provider is found. The topmost component is not the last dead-end of the provider lookup, since we can also declare global dependencies in the bootstrap() function, as we will see later in this chapter.

Whenever a provider resolves with a type matching that token, it will return such type as a singleton, which will be therefore injected by the injector as a dependency. In fairness, the provider is not just a collection of key/value pairs coupling tokens with previously registered types, but a factory that instantiates these types and also instantiates each dependency's very own dependencies as well, in a sort of recursive dependency instantiation.

So, instead of instantiating the Playlist object manually, we could do this:

import { Component } from '@angular/core';
import { Playlist } from './playlist';

@Component({
  selector: 'music-player',
  templateUrl: './music-player.component.html',
  providers: [Playlist]
})
class MusicPlayerComponent {  
  constructor(public playlist: Playlist) {}
}

The providers property of the @Component decorator is the place where we can register dependencies on a component level. From that moment onwards, these types will be immediately available for injection at the constructor of that component and, as we will see next, at its own child components as well.

Injecting dependencies across the component tree

We have seen that the provider lookup is performed upwards until a match is found. A more visual example might help, so let's figure out that we have a music app component that hosts in its directives property (and hence its template) a music library component with a collection of all our downloaded tunes which also hosts, in its own directives property and template, a music player component so we can playback any of the tunes in our library.

.
├── MusicAppComponent()
│  └── MusicLibraryComponent()
│     └── MusicPlayerComponent()
...

Our music player component requires an instance of the Playlist object we mentioned before, so we declare it as a constructor parameter, conveniently annotated with the Playlist token.

.
├── MusicAppComponent()
│  └── MusicLibraryComponent()
│     └── MusicPlayerComponent(playlist: Playlist)
...

When the MusicPlayerComponent entity is instantiated, the Angular DI mechanism will go through the parameters in the component constructor with special attention to their type annotations. Then, it will check if that type has been registered in the component's provider property of the component decorator configuration. The code is as follows:

@Component({
  selector: 'music-player',
  providers: [Playlist]
})
class MusicPlayerComponent {
  constructor(public playlist: Playlist) {}
}

But, what if we want to reuse the Playlist type in other components throughout the same component tree? Maybe the Playlist type contains functionalities in its API that are required by different components at once across the application. Do we have to declare the token in the provider's property for each one? Fortunately not, since Angular 2 anticipates that necessity and brings transversal dependency injection through the component tree.

Note

In the previous section, we mentioned that components conduct a provider lookup upwards. This is because each component has its own built-in injector, which is specific to it. Nevertheless, that injector is in reality a child instance of the parent's component injector (and so on so forth), so it is fair to say that an Angular 2 application has not a single injector, but many instances of the same injector, so to say.

We need to extend the injection of the Playlist object to other components in the component tree in a quick and reusable fashion. Knowing beforehand that components perform a provider lookup starting from itself and then passing up the request to its parent component's injectors, we can then address the issue by registering the provider in the parent component, or even the top parent component, so the dependency will be available for injection for each and every child component found underneath it. In this sense, we could register the Playlist object straight at MusicAppComponent, regardless it might not need it for its own implementation:

@Component({
  selector: 'music-app',
  providers: [Playlist],
  directives: [MusicLibraryComponent],
  template: '<music-library></music-library>'
})
class MusicAppComponent {}

The immediate child component might not require the dependency for its own implementation either. Since it has been already registered in its parent MusicAppComponent component, there is no need to register it there again.

@Component({
  selector: 'music-library',
  directives: [MusicPlayerComponent],
  template: '<music-player></music-player>'
})
class MusicLibraryComponent {}

We finally reach our music player component, but now it no longer features the Playlist type as a registered token in its providers property. In fact, our component does not feature a providers property at all. It no longer requires this, since the type has been already registered somewhere above the component's hierarchy, being immediately available for all child components, no matter where they are.

@Component({
  selector: 'music-player'
})
class MusicPlayerComponent {
  constructor(public playlist: Playlist) {}
}

Now, we see how dependencies are injected down the component hierarchy and how the provider lookup is performed by components just by checking their own registered providers and bubbling up the request upwards in the component tree. However, what if we want to constrain such injection or lookup actions?

Restricting dependency injection down the component tree

In our previous example, we saw how the music app component registered the Playlist token in its providers collection, making it immediately available for all child components. Sometimes, we might need to constrain the injection of dependencies to reach only those directives (and components) that are immediately next to a specific component in the hierarchy. We can do that by registering the type token in the viewProviders property of the component decorator, instead of using the providers property we've seen already. In our previous example, we can restrain the downwards injection of Playlist one level only:

@Component({
  selector: 'music-app',
  viewProviders: [Playlist],
  directives: [MusicLibraryComponent],
  template: '<music-library></music-library>'
})
class MusicAppComponent {}

We are informing Angular 2 that the Playlist provider should only be accessible by the injectors of the directives and components located in the MusicAppComponent view, but not for the children of such components. The use of this technique is exclusive of components, since only they feature views.

Restricting provider lookup

Just like we can restrict dependency injection, we can constrain dependency lookup to the immediate upper level only. To do so, we just need to apply the @Host() decorator to those dependency parameters whose provider lookup we want to restrict:

Import { Component, Host } from '@angular/core';

@Component({
  selector: 'music-player'
})
class MusicPlayerComponent {
  constructor(@Host() playlist: Playlist) {}
}

According to the preceding example, the MusicPlayerComponent injector will look up for a Playlist type at its parent component's providers collection (MusicLibraryComponent in our example) and will stop there, throwing an exception because Playlist has not been returned by the parent's injector (unless we also decorate it with the @Optional() parameter decorator).

Overriding providers in the injector hierarchy

We've seen so far how Angular's DI framework uses the dependency token to introspect the type required and return it right from any of the provider sets available along the component hierarchy. However, we might need to override the class instance corresponding to that token in certain cases where a more specialized type is required to do the job. Angular provides special tools to override the providers or even implement factories that will return a class instance for a given token, not necessarily matching the original type.

We will not cover all the use cases in detail here, but let's look at a simple example. In our example, we assumed that the Playlist object was meant to be available across the component tree for use in different entities of the application. What if our MusicAppComponent directive hosts another component whose child directives require a more specialized version of the Playlist object? Let's rethink our example:

.
├── MusicAppComponent()
│  ├── MusicChartsComponent()
│  │  └── MusicPlayerComponent()
│  └── MusicLibraryComponent()
│      └── MusicPlayerComponent()
...

This is a bit contrived example but will definitely help us to understand the point of overriding dependencies. The Playlist instance object is available right from the top component downwards. The MusicChartsComponent directive is a specialized component that caters only for music featured in the top seller's charts and hence its player must playback big hits only, regardless of the fact it uses the same component as MusicLibraryComponent. We need to ensure that each player component gets the proper playlist object, and this can be done at the MusicChartsComponent level by overriding the object instance corresponding to the Playlist token. The following example depicts this scenario, leveraging the use of the provide function:

import { Component, provide } from '@angular/core';
import { Playlist } from './playlist';
import { TopHitsPlaylist } from './top-hits-playlist';

@Component({
  selector: 'music-charts',
  directives: [MusicPlayerComponent],
  template: '<music-player></music-player>',
  providers: [provide(Playlist, { useClass: TopHitsPlaylist })]
})
class MusicChartsComponent {}

The provide() function creates a provider mapped to the token specified in the first argument (Playlist in this example) and the implementation configured in the second argument, which in this case points to using the TopHitsPlaylist type as the reference class.

We could refactor the block of code to use viewProviders instead, so we ensure that (if required) the child entities still receive an instance of Playlist instead of TopHitsPlaylist. Alternatively, we can go the extra mile and use a factory, to return the specific object instance we need depending on other requirements. The following example will return a different object instance for the Playlist token depending on the evaluation of a Boolean condition variable:

@Component({
  selector: 'music-charts',
  directives: [MusicPlayerComponent],
  template: '<music-player></music-player>',
  providers: [
    provide(Playlist, { useFactory: () => {
        if(condition) {
          return new TopHitsPlaylist();
        } else {
          return new Playlist();
        }
      }
    })
  ]
})
class MusicChartsComponent {}

Moving out from the preceding pseudo-code example, how can we provide a better logic flow when using the useFactory function? It turns out that the method signature can take arguments that operate pretty much the same as dependencies do when in the constructor of any given Angular entity. We just need to point Angular to the type each argument token of the useFactory lambda function has by declaring them in the deps property as follows:

@Component({
  selector: 'music-charts',
  directives: [MusicPlayerComponent],
  template: '<music-player></music-player>',
  providers: [
    ConditionalService,
    provide(Playlist, { useFactory: (conditionalService) => {
        if(conditionalService.isTopHits) {
          return new TopHitsPlaylist();
        } else {
          return new Playlist();
        }
      },
      deps: [ConditionalService]
    })
  ]
})
class MusicChartsComponent {}

In the preceding example, we are injecting an object instance of an imaginary ConditionalService class, which exposes a Boolean property named isTopHits that will inform about the playlist to be used. Keep in mind that these types will have to be registered as well, either in the providers property of the current component or at any of its parent components.

Extending injector support to custom entities

Directives and components require dependencies to be introspected, resolved, and injected. Other entities such as service classes often require quite such functionality too. In our example, our Playlist class might rely on a dependency on a HTTP client to communicate with a third party to fetch the songs. The action of injecting such dependency should be as easy as declaring the annotated dependencies in the class constructor and have an injector ready to fetch the object instance by inspecting the class provider or any other provider available somewhere.

It is only when we think hard about the latter that we realize there is a gap in this idea: custom classes and services do not belong to the component tree. Hence, they do not benefit from anything such as a built-in injector or a parent injector. We cannot even declare a providers property, since we do not decorate these types of class with a @Component or @Directive decorator. Let's take a look at an example:

class Playlist {
  songs: string[];
  constructor(songsService: SongsService) {
    this.songs = songsService.fetch(); 
  }
}

We might try the above in the hope of having Angular 2's DI mechanism introspecting the songsService parameter of the Playlist class constructor when instantiating this class in order to inject it into MusicPlayerComponent. Unfortunately, the only thing we will eventually get is an exception like this:

Cannot resolve all parameters for Playlist(?). Make sure they all have valid type or annotations.

This is kind of misleading, since all constructor parameters in Playlist have been properly annotated, right? As we said before, the Angular DI machinery resolves dependencies by introspecting the types of the constructor parameters. To do so, it needs some metadata to be created beforehand. Each and every Angular entity class decorated with a decorator features this metadata as a by-product of the way TypeScript compiles the decorator configuration details. However, dependencies that also require other dependencies have no decorator whatsoever and no metadata is then created for them. This can be easily fixed thanks to the @Injectable() decorator, which will give visibility to these service classes for the DI mechanism:

import { Injectable } from '@angular/core';

@Injectable()
class Playlist {
  songs: string[];
  constructor(songsService: SongsService) {
    this.songs = songsService.fetch(); 
  }
}

You will get used to introducing that decorator in your service classes, since they will quite often rely on other dependencies not related to the component tree in order to deliver the functionality.

Tip

It is actually a good practice to decorate all your service classes with the @Injectable() decorator, irrespective of whether its constructor functions have dependencies or not. This way, we prevent errors and exceptions because of skipping this requirement once the service class grows and requires more dependencies in the future.

Initializing applications with bootstrap()

As we have seen in this chapter, the dependency lookup bubbles up until the first component at the top. This is not exactly true, since there is an additional step that the DI mechanism will check on: the bootstrap() function.

As far as we know, we use the bootstrap()function to kickstart our application by declaring in its first argument the root component that initiates the application's component tree. However, the bootstrap function takes a second argument in the form of a providers array, where we can explicit dependencies as well, that will become available throughout the injector tree. However, this is a bad practice because it couples the availability of any provider to the application itself, constraining the encapsulation and reusability of our components, moreover when the bootstrap initialization function is platform-specific.

Where shall we declare our application dependencies then? Always use our top root component instead. The providers argument of the bootstrap function should only be used when we need to override existing Angular 2 providers on an application level, leveraging the provide() function, for instance.

Always keep in mind that we can have multiple root components, each one of them the result of multiple executions of the bootstrap() function declaring a different root component each time. Each one of these root components will feature its own set of injectors and service singletons, with no relationship whatsoever amongst them.

Switching between development and production modes

Angular 2 applications are bootstrapped and initialized by default in development mode. In the development mode, the Angular 2 runtime will throw warning messages and assertions to the browser console. While this is quite useful for debugging our application, we do not want those messages to be displayed when the application is in production. The good news is that the development mode can be disabled in favor of the more silent production mode. This action is usually performed before bootstrapping our application:

import { bootstrap } from '@angular/platform-browser-dynamic';
import { enableProdMode } from 'angular/core';
import AppComponent from './app.component';

enableProdMode();

bootstrap(AppComponent, []);

Enabling Angular 2's built-in change detection profiler

We can also access advanced tools from the browser console by enabling the Angular Debug Tools. To do so, just import the enableDebugTools function, which is specific in to the browser platform, and execute it as soon as you get hold of an instance of the component you want to profile. The code is as follows:

import { bootstrap } from '@angular/platform-browser-dynamic';
import { enableDebugTools } from '@angular/platform-browser';
import { ComponentRef } from 'angular/core';
import AppComponent from './app.component';

// The bootstrap() function returns a promise with 
// a reference to the bootstrapped component
bootstrap(AppComponent, []).then((ref: ComponentRef) => {
  enableDebugTools(ref);
});

When the enableDebugTools() function is triggered, you just need to follow the following steps to access the change detection profiler:

  1. Open the browser dev tools and switch to the console view.
  2. Type ng.profiler.timeChangeDetection({record: true}) and then press Enter.
  3. The Angular 2 runtime will exercise change detection in a loop and will print the average amount of time a single round of change detection takes for the current state of the UI. A CPU profile recording will be conducted while the change detector is exercised.

Hopefully, the debug tools will be fleshed out with more functionalities in the future. Stay tuned and refer to the official documentation.

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

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