Angular Dependency Injection

Direct Software Solutions
6 min readJan 14, 2023

Introduction

Dependency Injection is frequently abbreviated as DI. The paradigm is present throughout Angular. It maintains code flexibility, testability, and mutability. External logic can be inherited by classes without understanding how to generate it. Consumers of those classes are not required to know anything.

DI spares both courses and customers from having to learn more than is absolutely necessary. Despite this, the code is still as modular as it was before, owing to the DI features in Angular.

DI benefits heavily from services. They rely on the paradigm to inject into different customers. These customers can then take advantage of the service and/or forward it to others.

Service is not the only option. Directives, pipes, components, and so on: every Angular scheme benefits from DI in some manner.

Injectors

Injectors are data structures that hold instructions that specify where and how services are formed. They serve as middlemen inside the Angular DI mechanism.

Injector-specific metadata may be found in module, directive, and component classes. Each of these classes comes with a new injector instance. In this method, the application tree reflects the injector hierarchy.

The providers: [] metadata accepts services, which subsequently register with the injector of the class. This provider field contains the instructions required for an injector to work. A class instantiates a service by using its class as its data type (assuming it has dependencies). The injector aligns this type and produces a service instance on behalf of the class.

Of course, the class can only instantiate what the injector instructs it to instantiate. If the injector in the class does not have the service registered, it asks its parent. So on and so forth until either an injector with the service or the application root is reached.

Any injector in the application can register services. The providers: [] metadata field of class modules, directives, or components contains services. A service registered in the class’ injector can be instantiated by the class’ children. After all, child injectors rely on parent injectors.

Dependency Injection

Examine the skeletons for the following classes: service, module, directive, and component.

// application.service.ts
import { Injectable } from ‘@angular/core’;
@Injectable({
providedIn: /* injector goes here */
})
export class ApplicationService {
constructor() { }
}

// application.module.ts
import { NgModule } from ‘@angular/core’;
import { CommonModule } from ‘@angular/common’;
@NgModule({
imports: [
CommonModule
],
declarations: [],
providers: [ /* Services goes here */]
})
export class ApplicationModule { }

// application.directive.ts
import { Directive } from ‘@angular/core’;
@Directive({
selector: ‘[appicationRoot]’,
providers: [ /* Services goes here */]
})
export class ApplicationDirective {
constructor() { }
}

// application.component.ts
import { Component } from ‘@angular/core’;
@Component({
selector: ‘application- root’,
templateUrl: ‘./application.component.html’,
styleUrls: [‘./ application.component.css’],
providers: [ /* Services goes here */]
})
export class ApplicationComponent {
}

Each skeleton has the ability to register services with an injector. ApplicationService is, in reality, a service. Services may now register with injectors using @Injectable metadata in Angular 6.

Take note of the metadata providedIn: string ( @Injectable ) and providers: [] ( @Directive , @Component , and @Module ). They instruct injectors on where and how to generate services. Injectors would be unable to instantiate if this were not the case.

What if a service is dependent on another? What would the outcomes be? Providers respond to these questions so that injectors can correctly instantiate.

The DI framework is supported by injectors. They save instructions for instantiating services so that customers do not have to. They obtain service instances without being aware of the source dependence!

I should also mention that dependency injection may be used in other schemes that lack injectors. They are unable to register new services, but they may still instantiate from injectors.

Service

The @Injectable providedIn: string information defines which injector to register with. Using this way, the service may or may not register with the injector, depending on whether or not it is used. Angular refers to this as tree-shaking.

The value is set to ‘root’ by default. This corresponds to the application’s root injector. Setting the field to ‘root’ makes the service available from wherever.

Module, Directive, and Component

Each module and component has its own injector instance. Given the providers: [] metadata field, this is obvious. This field accepts an array of services and registers them with the module or component class’s injector. This method is used in the @NgModule , @Directive , and @Component decorators.

This technique does not include tree-shaking or the optional removal of unwanted injector services. Service instances rely on injectors for the duration of the module or component.

Instantiating References

Any class may be used to instantiate DOM references. Remember that references are still services. They are distinct from ordinary services in that they represent the status of something else. These services involve interactions with their references.

DOM references are constantly required by directives. These references allow directives to conduct modifications on their host elements. Consider the following illustration. The injector of the directive injects a reference to the host element into the class’ constructor.

// highlight.directive.ts
import { Directive, ElementRef, Renderer2, Input } from ‘@angular/core’;
@Directive({
selector: ‘[highlight]’
})
export class HighlightDirective {
constructor(
private renderer: Renderer2,
private host: ElementRef
) { }
@Input() set highlight(color: string) {
this.renderer.setStyle(this.host.nativeElement, ‘background - color’, color);
}
}
// app.component.html
This is a < span[highlight]=”’yellow’”> Highlighted Text! < /span>

Renderer2 is also instantiated. These services are provided by which injector? The source code for each service is taken from @angular/core . These services must then register with the root injector of the application.

import { BrowserModule } from ‘@angular/platform-browser’;
import { NgModule } from ‘@angular/core’;
import { AppComponent } from ‘./app.component’;
import { HighlightDirective } from ‘./highlight.directive’;
@NgModule({
declarations: [
AppComponent,
HighlightDirective
],
imports: [
BrowserModule
],
providers: [],
bootstrap: [
AppComponent
]
})
export class AppModule { }

An empty array of providers!? Not to worry. Angular automatically registers numerous services with the root injector. This contains Renderer2 and ElementRef . In this example, we manage the host element via its interface, which is derived from the instantiation of ElementRef . Renderer2 allows us to update the DOM through Angular’s view model.

Instantiating Services

This part is explained in some detail in the article Services and Injectors. However, for the most part, this section rehashes the prior section. Services will frequently relate to something else. They may just as well provide an interface for expanding the functionality of a class.

The following example will define a logging service that is introduced to the injector of a component via its providers: [] metadata.

// logger.service.ts
import { Injectable } from ‘@angular/core’;
@Injectable()
export class LoggerService {
stack: Array<string>() = [];
addLog(message: string): void {
this.stack = [message].concat(this.stack);
this.printHead();
}
clear(): void {
this.printLog();
this.stack = [];
}
private printHead(): void {
console.log(this.stack[0] || null);
}
private printLog(): void {
this.stack.reverse().forEach((log) => console.log(message));
}
}
// app.component.ts
import { Component } from ‘@angular/core’;
import { LoggerService } from ‘./services/logger.service’;
@Component({
selector: ‘app- root’,
templateUrl: ‘./app.component.html’,
providers: [LoggerService]
})
export class AppComponent {
constructor(private logger: LoggerService) { }
log(event: any, message: string): void {
event.preventDefault();
this.logger.addLog(`Message: ${message}`);
}
clearLog(): void {
this.logger.clear();
}
}

Pay attention to the AppComponent constructor and metadata. The provider’s metadata field containing LoggerService contains instructions for the component injector. The injector then knows the LoggerService to create from the constructor request.

The injector detects the type LoggerService in the constructor parameter loggerService. The injector then performs the stated instantiation.

Conclusion

A paradigm is dependency injection (DI). In Angular, it operates through a hierarchy of injectors. A class obtains its resources without needing to produce or be aware of them. Injectors receive instructions and, based on the service requested, instantiate it.

DI appears frequently in Angular. The Angular documentation explains why the paradigm is so popular. They go on to describe additional use-cases for DI in Angular that go well beyond what was covered in this article. Check it out on GitHub by clicking the link below! Visit our website to learn more about our team and our consulting services at: https://www.direct-software-solutions.com/

--

--