logo

Dependency Injection in Angular

As you may already know, Angular has its own Dependency Injection (DI) framework. At one of the projects we needed to deal with some of the subtleties of its work. I want to share this experience.

In this article, I want to show some tricks of using dependency injection in Angular. They help to reuse the code. Also, we’ll get a deeper understanding of how DI works in Angular.

The article will consist of several sections. In the theoretical section we’ll describe the ideas behind DI. In the second section, we’ll give you a partial summary of Angular documentation on its DI framework. The third section will focus on the code examples. Let’s hit the road!

Theory

First, we need to figure out what dependency injection is. Dependency injection is the process of providing an external dependency to a class. With this approach a client class does not create its own dependencies. It expects that at time of its creation dependencies are provided to it by some other entity (injector).

Dependency injection relates to the single responsibility and the inversion of control principles. The single responsibility principle states that each module / class should be responsible for one part of the program’s functionality and all this functionality should be concentrated in it. In particular, the client does not have to know anything about creating the dependencies, that is not part of its functionality. Inversion of control recommends inverting the control flow of the program. It means that the code solving specific problems is called from a more general one. For example, the abstract code from a framework, controls the invocation of a specific code written for the application. Thus, inversion occurs - a more general code determines how to call a more specific one.

What are the advantages of the program, where the creation of dependencies is separated from their use? Firstly, it’s testing modularity. For example, we have a TableComponent that uses a DataService, an OrderingService and a SearchService. We should test only the Component itself and pass the Services as mocks. Now, if the test fails, we know for sure that the problem is in the TableComponent, not in one of its Services. Secondly, we can change the implementation of the DataService independently of the TableComponent. We can also provide the TableComponent with another Service with the same interface. Imagine that TableComponent is a common component of tables. Then we can use FootballDataService and BasketballDataService with it if they share interfaces, without changing anything in TableComponent. All we need to do is configure the DI framework to resolve necessary service.

Now that we’ve figured out what dependency injection is, I suggest investigating how the dependency injection framework works in Angular.

DI in Angular

Let’s recall some general concepts. To make a class a service (in the DI terms) we use the @Injectable decorator on it. Other services, as well as classes decorated by @Component and @Directive decorators, can act as clients.

Angular injectors are configured using providers. A provider is an entity associating a certain token, by which a dependency can be obtained, with a specific dependency during runtime. Providers can be defined inside the providers parameter of the @NgModule, @Component, @Directive decorators. The easiest way to configure the provider is to specify the name of the desired service in one of the above places. This is implicitly converted to {provide: ServiceName, useClass: ServiceName}. As a result, we have a dependency ServiceName, that injector could retrieve by name ‘ServiceName’.

Angular has 2 hierarchies of injectors: ModuleInjector and ElementInjector. The ModuleInjector hierarchy is configured using the providers parameter in the @NgModule and @Injectable decorators. The scope of services in this hierarchy is an entire module. ElementInjector hierarchy is created for each DOM element. It is always empty unless providers are configured in the @Component or @Directive decorator. The scope of this hierarchy is a component and components used in it.

Injector dependency resolution is hierarchical. If the injector cannot provide the dependency, it will ask for its parent injector to provide it. And so on down to the root. First search is conducted through the ElementInjector hierarchy, then, if the service is not found, through the ModuleInjector hierarchy.

Code

So, we made our way through the jungle of theory to the open plains of practice. Let’s start and see how the above relates to our work. Techniques, examples of which are below, were used in our projects.

Case 1. Use of the ElementInjector hierarchy to create a service that is unique to a particular instance of a component and its subcomponents.

What’s happening?

We configure the provider at the OuterComponent component level. The OuterComponent and the InnerComponent will use a common instance of this service. It is inaccessible to the rest of the application.

What is this for?

This allows you to use several OuterComponents on the same page, with separate InnerService for each.

outmost.component.ts

import { Component } from '@angular/core';
import { InnerService } from './inner.service';

@Component({
    selector: 'app-outmost',
    // below we configure providers for injecting the InnerService through the ElementInjector
    // that will allow us to inject the InnerService in this component and all the components it uses (InnerComponent)
    // other components won’t have access to this service, even if they are in same module as this component
    // please keep in mind that destroying this component will destroy the service
    // recreating component would recreate service as well, so all data you put in the service will be lost
    providers: [
        InnerService,
    ],
    template:
         '<p>Last click was in inner component with index {{ service.lastClicked }}</p>'
         '<app-inner [index]="0"></app-inner>'
         '<app-inner [index]="1"></app-inner>',
})
export class OutmostComponent {
    constructor(public service: InnerService) { }
}

inner.component.ts

import { Component } from '@angular/core';
import { InnerService } from './inner.service';

@Component({
    selector: 'app-inner',
    template:  '<button (click)="service.click(index)">Click on inner component with index {{ index }}</button>',
})
export class InnerComponent {
    constructor(public service: InnerService) { }
}

inner.service.ts

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

@Injectable()
export class InnerService {
    private lastCLickedIdx = 0;

    public get lastClicked() { return this.lastClickedIdx; }

    public click(idx: number) { this.lastClickedIdx = idx; }
}

Case 2. Token Injection

What’s happening?

We inject the service by the token.

What is this for?

When using TableComponent in several places, we can transfer different services with the same interface on the same token. This will help reuse TableComponent code with different data. In PlayerModule, TableComponent will use PlayerService, and in TeamModule, it will use TeamService

player.module.ts

import { NgModule } from '@angular/core';
import { NEEDED_SERVICE, PlayerService } from './needed.service';
import { TableComponent } from './app.component';

@NgModule({
    Imports: [
        // Some module, that contains TableComponent
    ]
    providers: [
        {
            provide: NEEDED_SERVICE,
            useClass: PlayerService,
        },
    ],
})
export class PlayerModule { }

team.module.ts

import { NgModule } from '@angular/core';
import { NEEDED_SERVICE, PlayerService } from './needed.service';
import { TableComponent } from './app.component';

@NgModule({
    Imports: [
        // Some module, that contains TableComponent
    ]
    providers: [
        {
            provide: NEEDED_SERVICE,
            useClass: TeamService,
        },
    ],
})
export class TeamModule { }

team.service.ts

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

@Injectable()
export class PlayerService implements NeededServiceInterface {
    public getData() { return [
        ['Christiano Ronaldo', 'Forward'],
        ['Manuel Neuer', 'Goalkeeper'],
    ]; }
}

@Injectable()
export class TeamService implements NeededServiceInterface {
    public getData() { return [
        ['Manchester United F.C.', 'England'],
        ['FC Barcelona', 'Spain'],
    ]; }
}

export interface NeededServiceInterface {
    getData: () => Array<Array<any>>;
}

export const NEEDED_SERVICE = new InjectionToken<NeededServiceInterface>('needed service');

table.component.ts


import { Component, Inject } from '@angular/core';
import { NEEDED_SERVICE, NeededServiceInterface } from './needed.service';

@Component({
    selector: 'app-table',
    template:
'<div *ngFor=”let row of service.getData()”>’
‘<div>row[0]</div><div>row[1]</div></div>',
})
export class TableComponent {
    constructor(@Inject(NEEDED_SERVICE) public service: NeededServiceInterface) { }
}

Conclusion

So, let’s go through the theses in this article once again:

  1. Dependency injection is the process of providing an external dependency to a class.
  2. In the implementation of dependencies involved 3 entities: client, service, injector.
  3. In Angular, services are marked with the @Injectable directive. Clients can be a service, component, or directive.
  4. Provider is an entity that establishes a relationship between a DI token and a specific dependency at runtime.
  5. Injectors in Angular are hierarchical and there are 2 hierarchies: ElementInjector and ModuleInjector.