Angular
Services & DI
Class without DI
export class Car {
public engine: Engine;
public wheels: Wheel[];
private readonly description: 'without DI';
constructor() {
this.engine = new Engine();
this.wheels = new Array(4).fill(null).map(() => new Wheel());
}
drive() {
return `${this.description}: car with ` +
`${this.engine.volume} engine and ` +
`${this.wheels[0].material} wheels`;
}
}
Class with DI
export class CarWithDI {
private readonly description: 'with DI';
constructor(public engine: Engine, public wheels: Wheels) {}
drive() {
return `${this.description}: car with ` +
`${this.engine.volume} engine and ` +
`${this.wheels[0].material} wheels`;
}
}
let car = new CarWithDI(getInstance(Engine), getInstance(Wheels))
getInstance - gets existing instance of creates a new one
Services
- Contains any value, function, or feature that an app needs
- Should do something specific and do it well
- Singleton
Services
Must be provided in order to use it
import { Injectable } from '@angular/core';
@Injectable()
export class FooService {
bar() {
// Some magic happens
}
}
import { Component } from '@angular/core';
import { FooService } from '../services/foo-service';
@Component({ ... })
export class FooComponent {
constructor(private fooService: FooService) {}
}
import { NgModule } from '@angular/core';
@NgModule({
declarations: [ ... ],
imports: [ ... ],
providers: [ FooService ],
bootstrap: [ FooComponent ]
})
export class FeatureModule { }
providers...
@NgModule({
providers: [ s1, s2 ]
})
Module A
@NgModule({
imports: [ ModuleA ]
providers: [ s3 ],
})
Module B
@NgModule({
imports: [ ModuleB ],
providers: [ s4 ],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
AppModule
Root Injector
s1, s2, s3, s4
s1, s2, s3
Scope
s1, s2
providedIn
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class UserService {
}
import { Injectable } from '@angular/core';
import { UserModule } from './user.module';
@Injectable({
providedIn: UserModule,
})
export class UserService {
}
The example above shows the preferred way to provide a service in a module. This method is preferred because it enables tree-shaking of the service if nothing injects it.
Dependency Providers
import { NgModule } from '@angular/core';
import { FooService } from 'services/foo-service';
@NgModule({
declarations: [ ... ],
imports: [ ... ],
providers: [
FooService,
{ provide: 'SecondInstance', useClass: FooService }
],
bootstrap: [ FooComponent ]
})
export class FeatureModule { }
import { Component, Inject } from '@angular/core';
import { FooService } from '../services/foo-service';
@Component({ ... })
export class FooComponent {
constructor(private fooService: FooService,
@Inject('SecondInstance') private secondFooService: FooService) {}
}
useExisting
import { NgModule } from '@angular/core';
import { FooService } from '../services/foo-service';
@NgModule({
declarations: [ ... ],
imports: [ ... ],
providers: [
FooService,
{ provide: 'SecondToken', useExisting: FooService }
],
bootstrap: [ FooComponent ]
})
export class FeatureModule { }
import { Component, Inject } from '@angular/core';
import { FooService } from '../services/foo-service';
@Component({ ... })
export class FooComponent {
constructor(@Inject('SecondToken') private secondFooService: FooService) {}
}
UseValue, UseFactory, UseClass
import { NgModule } from '@angular/core';
import { FooService } from 'services/foo-service';
import { AnotherFooService } from 'services/another-foo-service';
import { AnotherImplementationService} from 'services/another-service';
function myFactory(IS_PROD: boolean) {
return IS_PROD ? new FooService() : new AnotherImplementationService();
}
@NgModule({
declarations: [ ... ],
imports: [ ... ],
providers: [
{ provide: 'IS_PROD', useValue: true },
{ provide: FooService, useFactory: myFactory, deps: ['IS_PROD'] },
{ provide: FooService, useClass: AnotherFooService },
],
bootstrap: [ FooComponent ]
})
export class FeatureModule { }
InjectionToken
import { NgModule, InjectionToken } from '@angular/core';
import { FooService } from 'services/foo-service';
import { AnotherImplementationService} from 'services/another-service';
const IS_PROD = new InjectionToken<boolean>('isProd');
function myFactory(IS_PROD: boolean) {
return IS_PROD ? new FooService() : new AnotherImplementationService();
}
@NgModule({
declarations: [ ... ],
imports: [ ... ],
providers: [
{ provide: IS_PROD, useValue: true },
{ provide: FooService, useFactory: myFactory, deps: ['IS_PROD']}
],
bootstrap: [ FooComponent ]
})
export class FeatureModule { }
Supply a custom provider
import { Inject, Injectable, InjectionToken } from '@angular/core';
export const BROWSER_STORAGE = new InjectionToken<Storage>('Browser Storage', {
providedIn: 'root',
factory: () => localStorage
});
@Injectable({
providedIn: 'root'
})
export class BrowserStorageService {
constructor(@Inject(BROWSER_STORAGE) public storage: Storage) {}
get(key: string) {
return this.storage.getItem(key);
}
set(key: string, value: string) {
this.storage.setItem(key, value);
}
remove(key: string) {
this.storage.removeItem(key);
}
clear() {
this.storage.clear();
}
}
forwardRef
@Injectable()
class Door {
lock: Lock;
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) {
this.lock = lock;
}
}
// Only at this point Lock is defined.
class Lock {}
const injector =
Injector.create({
providers: [
{ provide: Lock, deps: [] },
{ provide: Door, deps: [Lock] }
],
});
expect(injector.get(Door) instanceof Door).toBe(true);
expect(injector.get(Door).lock instanceof Lock).toBe(true);
Allows to refer to references which are not yet defined.
Optional Dependencies
import { Injectable, Optional } from '@angular/core';
import { FooService } from './data-service';
@Injectable()
export class MyDataService {
public data: any;
constructor(@Optional() private dataService: DataService) {
this.data = this.dataService ? this.dataService.getData() : 'local';
}
getData() {
return this.data;
}
}
@Host
class OtherService {}
class HostService {}
@Directive({selector: 'child-directive'})
class ChildDirective {
logs: string[] = [];
constructor(@Optional() @Host() os: OtherService, @Optional() @Host() hs: HostService) {
// os is null: true
this.logs.push(`os is null: ${os === null}`);
// hs is an instance of HostService: true
this.logs.push(`hs is an instance of HostService: ${hs instanceof HostService}`);
}
}
@Component({
selector: 'parent-cmp',
viewProviders: [HostService],
template: '<child-directive></child-directive>',
})
class ParentCmp {
}
@Component({
selector: 'app',
viewProviders: [OtherService],
template: '<parent-cmp></parent-cmp>',
})
class App {
}
Parameter decorator on a view-provider parameter that tells the DI framework to resolve the view by checking injectors of child elements, and stop when reaching the host element of the current component.
@Self
class Dependency {}
@Injectable()
class NeedsDependency {
constructor(@Self() public dependency: Dependency) {}
}
let inj = Injector.create({
providers: [
{provide: Dependency, deps: []},
{provide: NeedsDependency, deps: [[new Self(), Dependency]]}
]
});
const nd = inj.get(NeedsDependency);
expect(nd.dependency instanceof Dependency).toBe(true);
const child = Injector.create({
providers: [{provide: NeedsDependency, deps: [[new Self(), Dependency]]}],
parent: inj
});
expect(() => child.get(NeedsDependency)).toThrowError();
Parameter decorator to be used on constructor parameters, which tells the DI framework to start dependency resolution from the local injector.
@SkipSelf
class Dependency {}
@Injectable()
class NeedsDependency {
constructor(@SkipSelf() public dependency: Dependency) {}
}
const parent = Injector.create({providers: [{provide: Dependency, deps: []}]});
const child =
Injector.create({providers: [{provide: NeedsDependency, deps: [Dependency]}], parent});
expect(child.get(NeedsDependency).dependency instanceof Dependency).toBe(true);
const inj = Injector.create(
{providers: [{provide: NeedsDependency, deps: [[new Self(), Dependency]]}]});
expect(() => inj.get(NeedsDependency)).toThrowError();
Parameter decorator to be used on constructor parameters, which tells the DI framework to start dependency resolution from the parent injector. Resolution works upward through the injector hierarchy, so the local injector is not checked for a provider.
Useful links
Angular. Services & DI
By Pavel Razuvalau
Angular. Services & DI
- 418