Lab: Services and Dependency Injection - Deep Dive
- Lab: Services and Dependency Injection - Deep Dive
Tree-shakable Providers and useClass
-
Add a
DefaultFlightServiceand aDummyFlightService:ng generate service default-flight ng generate service dummy-flight -
Open the existing
flight.service.tsfile and convert it into a abstract class:// src/app/flight.service.ts import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { DefaultFlightService } from './default-flight.service'; import { Flight } from './flight'; @Injectable({ providedIn: 'root', // Add this redirection: useClass: DefaultFlightService, }) // Make class abstract: export abstract class FlightService { // This methods becomes abstract too: abstract find(from: string, to: string): Observable<Flight[]>; } -
Open the file
default-flight.service.ts. Let theDefaultFlightServiceimplement the abstractFlightServiceclass:// src/app/default-flight.service.ts import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Flight } from './flight'; import { FlightService } from './flight.service'; @Injectable({ providedIn: 'root', }) export class DefaultFlightService implements FlightService { constructor(private http: HttpClient) {} find(from: string, to: string): Observable<Flight[]> { const url = 'http://demo.ANGULARarchitects.io/api/flight'; const headers = new HttpHeaders().set('Accept', 'application/json'); const params = new HttpParams().set('from', from).set('to', to); return this.http.get<Flight[]>(url, { headers, params }); } } -
Open the
FlightSearchComponentand assure yourself that it uses the abstractFlightServiceas the injection token. -
Start your application and check if the FlightSearchComponents gets the
DefaultFlightServiceinjected. -
Now, let's switch to a different implementation of the
FlightService.For this, open the file
dummy-flight.service.tsand let it also implement the abstractFlightService:// src/app/dummy-flight.service.ts import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { Flight } from './flight'; import { FlightService } from './flight.service'; @Injectable({ providedIn: 'root', }) export class DummyFlightService implements FlightService { constructor() {} find(from: string, to: string): Observable<Flight[]> { return of([ { id: 1, from: 'Frankfurt', to: 'Flagranti', date: '2022-01-02T19:00+01:00' }, { id: 2, from: 'Frankfurt', to: 'Kognito', date: '2022-01-02T19:30+01:00' }, { id: 3, from: 'Frankfurt', to: 'Mallorca', date: '2022-01-02T20:00+01:00' }, ]); } } -
Open the
flight.service.tsfile again, and make it redirect to theDummyFlightService:// src/app/flight.service.ts import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { DummyFlightService } from './dummy-flight.service'; import { Flight } from './flight'; @Injectable({ providedIn: 'root', // Redirect to DummyFlightService for the time being: useClass: DummyFlightService, }) export abstract class FlightService { abstract find(from: string, to: string): Observable<Flight[]>; } -
Start your application (if it isn't still running) and check if the
DummyFlightServiceis now used by theFlightSearchComponent.
Traditional Providers and useClass
-
Open the file
flight.service.tsand remove the settings provided toInjectable:// src/app/flight.service.ts @Injectable() export abstract class FlightService { abstract find(from: string, to: string): Observable<Flight[]>; } -
Open the file
app.module.tsand add a traditional provider for theFlightService:// src/app/app.module.ts [...] // Import services: import { FlightService } from './flight.service'; import { DefaultFlightService } from './default-flight.service'; @NgModule({ imports: [ [...] ], declarations: [ [...] ], providers: [ // Add service Provides { provide: FlightService, useClass: DefaultFlightService } ], bootstrap: [ AppComponent ] }) export class AppModule { } -
Start the application (if it isn't still running) and assure yourself that the
FlightSearchComponentuses theDefaultFlightService. -
Undo the performed changes so that you are using Tree-shakable providers again. Also test this.
Tree-shakable Providers and useFactory
-
Open the file
app.module.tsand remove the configured traditional provider. -
Create a file
flight-service.factory.ts:// src/app/flight-service.factory.ts import { HttpClient } from '@angular/common/http'; import { DefaultFlightService } from './default-flight.service'; import { DummyFlightService } from './dummy-flight.service'; const DEBUG = false; export const createFlightService = (http: HttpClient) => { if (!DEBUG) { return new DefaultFlightService(http); } else { return new DummyFlightService(); } }; -
Open the file
flight.service.tsand add the following configuration to theInjectableprovider:// src/app/flight.service.ts [...] // New import: import { createFlightService } from './flight-service.factory'; @Injectable({ providedIn: 'root', useFactory: createFlightService, deps: [HttpClient] }) export abstract class FlightService { abstract find(from: string, to: string): Observable<Flight[]>; } -
Start your application and assure yourself that the used
FlightServiceis now created by your factory. For this, you can switch the value of the constantDEBUG.
Traditional Providers and useFactory
-
Open the
flight.service.tsfile and remove the configuration passed to theInjectableDecorator@Injectable() export abstract class FlightService { abstract find(from: string, to: string): Observable<Flight[]>; } -
Open the file
app.module.tsand introduce the following traditional provider:
// src/app/app.module.ts
[...]
// New import:
import { createFlightService } from './flight-service.factory';
@NgModule({
imports: [
[...]
],
declarations: [
[...]
],
providers: [
{
provide: FlightService,
useFactory: createFlightService,
deps: [HttpClient]
}
],
bootstrap: [
AppComponent
]
})
export class AppModule { }
- Start your application and assure yourself that the used
FlightServiceis still created by your factory. For this, you can switch the value of the constantDEBUG.
DI-Scopes
Local Services
-
Open the file
flight-search.component.tsand introduce a local provider for theFlightSearchComponent:// src/app/flight-search/flight-search.component.ts [...] import { FlightService } from '../flight.service'; // Add this import: import { DummyFlightService } from '../dummy-flight.service'; @Component({ [...] // Add this: providers: [ { provide: FlightService, useClass: DummyFlightService } ] }) export class FlightSearchComponent implements OnInit { [...] constructor(private flightService: FlightService) { } [...] } -
Start your application (if it isn't still running) and assure yourself that the FlightSearchComponent uses the local
DummyFlightServiceagain. -
Remove the introduced local service in
flight-search.component.tsand assure yourself that the globalDefaultFlightServiceis used again.
Bonus: Multi-Providers and InjectionToken
Bonus: Multi-Providers *
-
Open the
flight.service.tsfile and remove the configuration passed to theInjectableDecorator (if it's still there):@Injectable() export abstract class FlightService { abstract find(from: string, to: string): Observable<Flight[]>; } -
Open the
app.module.tsfile and introduce the following multi-providers:[...] providers: [ { provide: FlightService, useClass: DefaultFlightService, multi: true }, { provide: FlightService, useClass: DummyFlightService, multi: true } ], [...] -
Open the file
flight-search.component.ts. Instead of aFlightService, inject now a FlightService Array (FlightService[]):// src/app/flight-search/flight-search.component.ts // Import Inject: import { Component, Inject, OnInit } from '@angular/core'; [...] @Component( [...] ) export class FlightSearchComponent implements OnInit { [...] // Change the next line: constructor(@Inject(FlightService) private flightServices: FlightService[]) { } search(): void { [...] } [...] }Please note, that we need to use Inject with the
FlightServicetype, because Arrays (e. g.FlightService[]) cannot be used as Tokens. Using constants of the typeInjectionToken<T>as shown in the next section can help here. -
Also, in the file
flight-search.component.ts, update the search method so that it uses allFlightServices in the injected array:// src/app/flight-search/flight-search.component.ts [...] // New import import { merge } from 'rxjs'; @Component( [...] ) export class FlightSearchComponent implements OnInit { constructor(@Inject(FlightService) private flightServices: FlightService[]) { } ngOnInit(): void { } search(): void { this.flights = []; const observables = this.flightServices.map(fs => fs.find(this.from, this.to)); // Merge results of individual observables: const observable = merge(...observables); // This is the same as: merge(observables[0], observables[1], ...) observable.subscribe({ next: (additionalFlights) => { this.flights = [...this.flights, ...additionalFlights]; // Same as: [this.flights[0], this.flights[1], ..., additionalFlights[0], additionalFlights[1], ...] }, error: (err) => { console.debug('Error', err); } }); } [...] } -
Start your application and assure yourself that both registered
FlightServiceimplementations are used.
Bonus: Tree-shakable Providers with InjectionToken Constants *
-
Create a file
tokens.ts:// src/app/tokens.ts import { InjectionToken } from '@angular/core'; export const BASE_URL = new InjectionToken<string>('BASE_URL', { providedIn: 'root', factory: () => 'http://demo.ANGULARarchitects.io/api/', }); -
Open the file
default-flight.service.tsand inject the definedBASE_URLInjectionToken:// src/app/default-flight.service.ts // Add this import: import { BASE_URL } from './tokens'; // Import Inject: import { Inject, Injectable } from '@angular/core'; [...] @Injectable({ providedIn: 'root' }) export class DefaultFlightService implements FlightService { constructor( private http: HttpClient, // Inject BASE_URL: @Inject(BASE_URL) private baseUrl: string, ) { } find(from: string, to: string): Observable<Flight[]> { const url = this.baseUrl + 'flight'; const headers = new HttpHeaders() .set('Accept', 'application/json'); const params = new HttpParams() .set('from', from) .set('to', to); return this.http.get<Flight[]>(url, {headers, params}); } [...] } -
Start your application (if it's not still running) and assure yourself the new token is used.
Bonus: Traditional Providers with InjectionToken Constants *
-
Open the file
tokens.tsand remove the configuration from theInjectionToken:export const BASE_URL = new InjectionToken<string>('BASE_URL'); -
Open the file
app.module.tsand introduce the following provider:providers: [ { provide: BASE_URL, useValue: 'http://demo.ANGULARarchitects.io/api/' } ], -
Start your application (if it's not still running) and assure yourself the new provider is used.
Bonus: Multi-Providers and InjectionToken *
As mentioned above, Arrays cannot be used a tokens. Hence, it's quite usual to combine Multi-Providers together with an InjectionToken
-
Open your
tokens.tsfile and add the followingInjectionToken:export const FLIGHT_SERVICES = new InjectionToken<FlightService>('FLIGHT_SERVICES'); -
Open the file
app.module.tsand introduce the following providers. If you already have them, update them accordingly:providers: [ { provide: FLIGHT_SERVICES, useClass: DefaultFlightService, multi: true }, { provide: FLIGHT_SERVICES, useClass: DummyFlightService, multi: true }, ], -
Now, switch to your
flight-search.component.tsand adjust the constructor as follows. Please note that@Injectpoints now to theFLIGHT_SERVICESInjectionTokenintroduced before:[...] constructor(@Inject(FLIGHT_SERVICES) private flightServices: FlightService[]) { } [...] -
Start your application (if it's not still running) and assure yourself the multi provider works.