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