Advanced Dependency Injection
Dependency Injection (DI) is a core concept in Angular that allows for better modularity and testability. This guide covers advanced DI topics such as multi-providers, tokens, and hierarchical injectors.
Setting Up Dependency Injection
First, ensure that your Angular application is set up to use Dependency Injection by importing the necessary modules:
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { DataService } from './data.service';
import { LoggingService } from './logging.service';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
providers: [DataService, LoggingService],
bootstrap: [AppComponent]
})
export class AppModule { }
Creating Services
Create services that you want to inject into your components and other services:
$ ng generate service data
$ ng generate service logging
This command generates new service files named data.service.ts
and logging.service.ts
.
Using Injection Tokens
Injection tokens are used to create custom tokens for DI. They are particularly useful when you need to inject non-class dependencies:
// app.module.ts
import { InjectionToken } from '@angular/core';
export const API_URL = new InjectionToken('apiUrl');
@NgModule({
...
providers: [
{ provide: API_URL, useValue: 'https://api.example.com' }
]
})
export class AppModule { }
// data.service.ts
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { API_URL } from './app.module';
@Injectable({
providedIn: 'root'
})
export class DataService {
constructor(private http: HttpClient, @Inject(API_URL) private apiUrl: string) {}
getData() {
return this.http.get(this.apiUrl);
}
}
Multi-Providers
Multi-providers allow you to provide multiple values for a single token. This can be useful for things like logging where you might have multiple loggers:
// logging.service.ts
import { InjectionToken } from '@angular/core';
export interface Logger {
log: (message: string) => void;
}
export const LOGGERS = new InjectionToken('loggers');
@Injectable({
providedIn: 'root'
})
export class ConsoleLoggerService implements Logger {
log(message: string) {
console.log('ConsoleLogger:', message);
}
}
@Injectable({
providedIn: 'root'
})
export class FileLoggerService implements Logger {
log(message: string) {
// Simulate writing to a file
console.log('FileLogger:', message);
}
}
// app.module.ts
@NgModule({
...
providers: [
{ provide: LOGGERS, useClass: ConsoleLoggerService, multi: true },
{ provide: LOGGERS, useClass: FileLoggerService, multi: true }
]
})
export class AppModule { }
// another.service.ts
import { Injectable, Inject } from '@angular/core';
import { LOGGERS, Logger } from './logging.service';
@Injectable({
providedIn: 'root'
})
export class AnotherService {
constructor(@Inject(LOGGERS) private loggers: Logger[]) {}
doSomething() {
this.loggers.forEach(logger => logger.log('Doing something'));
}
}
Hierarchical Injectors
Angular uses a hierarchical injector system, which means you can define providers at different levels of your application. This allows for different instances of a service to be injected in different parts of your application:
// app.module.ts
@NgModule({
...
providers: [DataService] // Root injector
})
export class AppModule { }
// feature.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FeatureComponent } from './feature.component';
import { DataService } from '../data.service';
@NgModule({
declarations: [FeatureComponent],
imports: [CommonModule],
providers: [{ provide: DataService, useClass: FeatureSpecificDataService }] // Feature injector
})
export class FeatureModule { }
// feature.component.ts
import { Component } from '@angular/core';
import { DataService } from '../data.service';
@Component({
selector: 'app-feature',
template: `...`,
})
export class FeatureComponent {
constructor(private dataService: DataService) {}
}
Using the Service in a Component
Inject the service into your component's constructor and use it:
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-root',
template: `Data
`
})
export class AppComponent implements OnInit {
constructor(private dataService: DataService) {}
ngOnInit() {
this.dataService.getData().subscribe(data => {
console.log(data);
});
}
}
Key Points
- Dependency Injection (DI) is a core concept in Angular that allows for better modularity and testability.
- Use Injection Tokens to create custom tokens for DI, especially for non-class dependencies.
- Multi-providers allow you to provide multiple values for a single token.
- Hierarchical injectors allow for different instances of a service to be injected in different parts of your application.
- Ensure your services are provided in the appropriate module or component to take advantage of Angular's hierarchical DI system.
Conclusion
Advanced dependency injection techniques in Angular allow you to build more modular, testable, and maintainable applications. By using features such as Injection Tokens, multi-providers, and hierarchical injectors, you can fine-tune your application's DI configuration to meet your needs. Happy coding!