16. Dependency Injection
TechnologiesService:
import { Injectable } from '@angular/core';
@Injectable()
export class TechnologiesService {
getTechnologies(): string[] {
return ['Angular 5', 'Angular CLI', 'Angular Material', 'Angular Universal'];
}
}
TechnologiesComponent:
import { Component, OnInit } from '@angular/core';
import { TechnologiesService } from './technologies.service';
@Component({
selector: 'app-technologies',
template: `
<h2>{{ title }}</h2>
<ul class="list-group">
<li *ngFor="let technology of technologies" class="list-group-item list-group-item-success">
{{ technology }}
</li>
</ul>
`,
providers: [ TechnologiesService ]
})
export class TechnologiesComponent implements OnInit {
title = 'List of Technologies';
technologies = [];
constructor(private technologiesService: TechnologiesService) { }
ngOnInit(): void {
this.technologies = this.technologiesService.getTechnologies();
}
}
Let's view the results in our Chromium Web Browser:
Spaceship No DI:
// spaceship without di
import { Engine, Wings } from './spaceship';
export class Spaceship {
public engine: Engine;
public wings: Wings;
public description = 'No DI';
constructor() {
this.engine = new Engine();
this.wings = new Wings();
}
fly() {
return `${this.description} spaceship with ` +
`${this.engine.cylinders} cylinders and ${this.wings.make} wings`;
}
}
Spaceship DI:
import { Injectable } from '@angular/core';
export class Engine {
public cylinders = 128;
}
export class Wings {
public make = 'Rocket';
public model = 'Booster';
}
@Injectable()
export class Spaceship {
public description = 'DI';
constructor(public engine: Engine, public wings: Wings) { }
// method using engine and wings
fly() {
return `${this.description} spaceship with ` +
`${this.engine.cylinders} cylinders and ${this.wings.make} wings`;
}
}
Spaceship Services:
import { Injectable } from '@angular/core';
export class Engine {
public cylinders = 128;
}
export class Wings {
public make = 'Rocket';
public model = 'Booster';
}
@Injectable()
export class Spaceship {
public description = 'DI';
constructor(public engine: Engine, public wings: Wings) { }
// method using engine and wings
fly() {
return `${this.description} spaceship with ` +
`${this.engine.cylinders} cylinders and ${this.wings.make} wings`;
}
}
Spaceship Creations:
// examples with spaceship and engine variations
import { Spaceship, Engine, Wings } from './spaceship';
////////////////// example 1 //////////////////
export function basicSpaceship() {
// basic spaceship with 128 cylinders and Rocket wings
const spaceship = new Spaceship(new Engine(), new Wings());
spaceship.description = 'Basic';
return spaceship;
}
///////////////// example 2 ////////////////////
class Engine2 {
constructor(public cylinders: number) { }
}
export function superSpaceship() {
// super spaceship with 512 cylinders and Rocket wings
const bigCylinders = 512;
const spaceship = new Spaceship(new Engine2(bigCylinders), new Wings());
spaceship.description = 'Super';
return spaceship;
}
/////////////// example 3 ////////////////////
class MockEngine extends Engine { cylinders = 1024; }
class MockWings extends Wings { make = 'Stealth'; }
export function prototypeSpaceship() {
// prototype spaceship with 1024 cylinders and Stealth wings
const spaceship = new Spaceship(new MockEngine(), new MockWings());
spaceship.description = 'Prototype';
return spaceship;
}
SpaceshipFactory:
import { Engine, Wings, Spaceship } from './spaceship';
// BAD pattern!
export class SpaceshipFactory {
createSpaceship() {
const spaceship = new Spaceship(this.createEngine(), this.createWings());
spaceship.description = 'Factory';
return spaceship;
}
createEngine() {
return new Engine();
}
createWings() {
return new Wings();
}
}
LoggerService:
import { Injectable } from '@angular/core';
@Injectable()
export class Logger {
logs: string[] = []; // capture logs for testing
log(message: string) {
this.logs.push(message);
console.log(message);
}
}
SpaceshipInjector:
import { ReflectiveInjector } from '@angular/core';
import { Spaceship, Engine, Wings } from './spaceship';
import { Logger } from '../logger.service';
export function useInjector() {
let injector: ReflectiveInjector;
injector = ReflectiveInjector.resolveAndCreate([Spaceship, Engine, Wings]);
const spaceship = injector.get(Spaceship);
spaceship.description = 'Injector';
injector = ReflectiveInjector.resolveAndCreate([Logger]);
const logger = injector.get(Logger);
logger.log(`Injector spaceship.drive() roared: ${spaceship.fly()}`);
return spaceship;
}
SpaceshipComponent:
import { Component } from '@angular/core';
import { Spaceship, Engine, Wings } from './spaceship';
import { Spaceship as SpaceshipNoDi } from './spaceship-no-di';
import { basicSpaceship, superSpaceship, prototypeSpaceship } from './spaceship-creations';
import { SpaceshipFactory } from './spaceship-factory';
import { useInjector } from './spaceship-injector';
@Component({
selector: 'app-spaceship',
template: `
<h2>Spaceships</h2>
<div id="di">{{ spaceship.fly() }}</div>
<div id="nodi">{{ noDiSpaceship.fly() }}</div>
<div id="basic">{{ basicSpaceship.fly() }}</div>
<div id="super">{{ superSpaceship.fly() }}</div>
<div id="prototype">{{ prototypeSpaceship.fly() }}</div>
<div id="factory">{{ factorySpaceship.fly() }}</div>
<div id="injector">{{ injectorSpaceship.fly() }}</div>
`,
providers: [ Spaceship, Engine, Wings ]
})
export class SpaceshipComponent {
noDiSpaceship = new SpaceshipNoDi;
basicSpaceship = basicSpaceship();
superSpaceship = superSpaceship();
prototypeSpaceship = prototypeSpaceship();
factorySpaceship = (new SpaceshipFactory).createSpaceship();
injectorSpaceship = useInjector();
constructor(public spaceship: Spaceship) { }
}
Let's view the results in our Chromium Web Browser:
A Spaceship:
MockTechnologies:
import { Technology } from './technology';
export const TECHNOLOGIES: Technology[] = [
{ id: 11, isSecret: false, name: 'Modules' },
{ id: 12, isSecret: false, name: 'Components' },
{ id: 13, isSecret: false, name: 'Templates' },
{ id: 14, isSecret: false, name: 'Metadata' },
{ id: 15, isSecret: false, name: 'Data Binding'},
{ id: 16, isSecret: false, name: 'Directives' },
{ id: 17, isSecret: false, name: 'Services' },
{ id: 18, isSecret: true, name: 'Dependency Injection' },
{ id: 19, isSecret: true, name: 'Animations' },
{ id: 20, isSecret: true, name: 'Forms' },
{ id: 21, isSecret: true, name: 'HTTP' },
{ id: 22, isSecret: true, name: 'Lifecycle Hooks' },
{ id: 23, isSecret: true, name: 'Pipes' },
{ id: 24, isSecret: true, name: 'Router' },
{ id: 24, isSecret: true, name: 'Testing' }
];
Technology:
export class Technology {
id: number;
name: string;
isSecret = false;
}
TechnologyListComponent without DI:
import { Component } from '@angular/core';
import { TECHNOLOGIES } from './mock-technologies';
@Component({
selector: 'app-my-technology-list',
template: `
<div *ngFor="let technology of technologies">
{{ technology.id }} - {{ technology.name }}
</div>
`
})
export class TechnologyListComponent {
technologies = TECHNOLOGIES;
}
TechnologyService:
import { Injectable } from '@angular/core';
import { TECHNOLOGIES } from './mock-technologies';
@Injectable()
export class TechnologyService {
getTechnologies() { return TECHNOLOGIES; }
}
TechnologyListComponent with DI:
import { Component } from '@angular/core';
import { Technology } from './technology';
import { TechnologyService } from './technology.service';
@Component({
selector: 'app-my-technology-list',
template: `
<div *ngFor="let technology of technologies">
{{ technology.id }} - {{ technology.name }}
</div>
`
})
export class TechnologyListComponent {
technologies: Technology[] = [];
constructor(technologyService: TechnologyService) {
this.technologies = technologyService.getTechnologies();
}
}
Let's view the results in our Chromium Web Browser:
TestComponent:
import { Component } from '@angular/core';
import { TechnologyService } from './technologies/technology.service';
import { TechnologyListComponent } from './technologies/technology-list.component';
@Component({
selector: 'app-tests',
template: `
<h2>Tests</h2>
<p id="tests">Tests {{ results.pass }}: {{ results.message }}</p>
`
})
export class TestComponent {
results = runTests();
}
///////////////////////////////////////////////
function runTests() {
const expectedTechnologies = [{name: 'Change Detection'}, {name: 'Events'}];
const mockService = <TechnologyService> { getTechnologies: () => expectedTechnologies };
it('should have technologies when TechnologyListComponent created', () => {
const tlc = new TechnologyListComponent(mockService);
expect(tlc.technologies.length).toEqual(expectedTechnologies.length);
});
return testResults;
}
//////////////////////////////////////////////
// Fake Jasmine Infrastructure
let testName: string;
let testResults: { pass: string; message: string };
function expect(actual: any) {
return {
toEqual: function(expected: any) {
testResults = actual === expected ? {pass: 'passed', message: testName} :
{pass: 'failed', message: `${testName}; expected ${actual} to equal ${expected}.`};
}
};
}
function it(label: string, test: () => void) {
testName = label;
test();
}
TechnologyService FactoryProvider:
import { Injectable } from '@angular/core';
import { TECHNOLOGIES } from './mock-technologies';
import { Logger } from '../logger.service';
@Injectable()
export class TechnologyService {
constructor(private logger: Logger, private isAuthorized: boolean) { }
getTechnologies() {
const auth = this.isAuthorized ? 'authorized' : 'unauthorized';
this.logger.log(`Getting technologies for ${auth} user.`);
return TECHNOLOGIES.filter(technology => this.isAuthorized || !technology.isSecret);
}
}
TechnologyServiceProvider FactoryProvider:
import { TechnologyService } from './technology.service';
import { Logger } from '../logger.service';
import { UserService } from '../user.service';
const technologyServiceFactory = (logger: Logger, userService: UserService) => {
return new TechnologyService(logger, userService.user.isAuthorized);
};
export const technologyServiceProvider = {
provide: TechnologyService, useFactory: technologyServiceFactory, deps: [Logger, UserService]
};
UserService FactoryProvider:
import { Injectable } from '@angular/core';
export class User {
constructor(public name: string, public isAuthorized = false) { }
}
const nils = new User('Nils', true);
const laura = new User('Laura', false);
@Injectable()
export class UserService {
user = laura; // initial user is laura
// swap users
getNewUser() {
return this.user = (this.user === laura) ? nils : laura;
}
}
TechnologiesComponent FactoryProvider:
import { Component } from '@angular/core';
import { technologyServiceProvider } from './technology.service.provider';
@Component({
selector: 'app-my-technologies',
template: `
<h2>My Technologies</h2>
<app-my-technology-list></app-my-technology-list>
`,
providers: [ technologyServiceProvider ]
})
export class TechnologiesComponent {
}
AppComponent FactoryProvider:
import { Component } from '@angular/core';
import { Logger } from './logger.service';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
template: `
<div class="container">
<div class="left">
<h1>{{ title }}</h1>
<app-spaceship></app-spaceship>
<app-tests></app-tests>
<h2>User</h2>
<p id="user">
{{ userInfo }}
<button (click)="nextUser()">Next User</button>
</p>
<app-my-technologies id="authorized" *ngIf="isAuthorized"></app-my-technologies>
<app-my-technologies id="unauthorized" *ngIf="!isAuthorized"></app-my-technologies>
</div>
<div class="right">
<img class="img-round" [src]="imageUrl" alt="princess image">
</div>
</div>
`
})
export class AppComponent {
imageUrl = '../assets/polymer3.jpg';
title = 'Tour Of Technologies';
constructor(private userService: UserService) { }
get isAuthorized() { return this.user.isAuthorized; }
nextUser() { return this.userService.getNewUser(); }
get user() { return this.userService.user; }
get userInfo() {
return `Current user, ${this.user.name}, is ` +
`${this.isAuthorized ? '' : 'not'} authorized.`;
}
}
Let's view the results in our Chromium Web Browser:
Other Injectors:
import { Component, Injector, OnInit } from '@angular/core';
import { Spaceship, Engine, Wings } from './spaceship/spaceship';
import { Technology } from './technologies/technology';
import { TechnologyService } from './technologies/technology.service';
import { technologyServiceProvider } from './technologies/technology.service.provider';
import { Logger } from './logger.service';
@Component({
selector: 'app-injectors',
template: `
<h2>Other Injections</h2>
<div id="spaceship">{{ spaceship.fly() }}</div>
<div id="technology">{{ technology.name }}</div>
<div id="android">{{ android }}</div>
`,
providers: [ Spaceship, Engine, Wings, technologyServiceProvider, Logger]
})
export class InjectorComponent implements OnInit {
spaceship: Spaceship;
technologyService: TechnologyService;
technology: Technology;
constructor(private injector: Injector) { }
ngOnInit() {
this.spaceship = this.injector.get(Spaceship);
this.technologyService = this.injector.get(TechnologyService);
this.technology = this.technologyService.getTechnologies()[0];
}
get android() {
const androidOnAllDesktops = `Android? I think they will take over all desktops in the future!`;
return this.injector.get(ANDROID, androidOnAllDesktops);
}
}
export class ANDROID { }
ProvidersComponent:
// example of providers array
import { Component, Inject, Injectable, OnInit } from '@angular/core';
import { APP_CONFIG, AppConfig, TECHNOLOGY_DI_CONFIG } from './app.config';
import { TechnologyService } from './technologies/technology.service';
import { technologyServiceProvider } from './technologies/technology.service.provider';
import { Logger } from './logger.service';
import { UserService } from './user.service';
const template = '{{ log }}';
///////////////////////////////////////////
@Component({
selector: 'app-provider-1',
template: template,
providers: [Logger]
})
export class Provider1Component {
log: string;
constructor(logger: Logger) {
logger.log('Hello from logger provided with Logger class');
this.log = logger.logs[0];
}
}
//////////////////////////////////////////
@Component({
selector: 'app-provider-3',
template: template,
providers: [{ provide: Logger, useClass: Logger}]
})
export class Provider3Component {
log: string;
constructor(logger: Logger) {
logger.log('Hello from logger provided with useClass: Logger');
this.log = logger.logs[0];
}
}
/////////////////////////////////////////
class BetterLogger extends Logger { }
@Component({
selector: 'app-provider-4',
template: template,
providers: [{ provide: Logger, useClass: BetterLogger }]
})
export class Provider4Component {
log: string;
constructor(logger: Logger) {
logger.log('Hello from logger provided with useClass: BetterLogger');
this.log = logger.logs[0];
}
}
////////////////////////////////////////
@Injectable()
class EvenBetterLogger extends Logger {
constructor(private userService: UserService) { super(); }
log(message: string) {
const name = this.userService.user.name;
super.log(`Message to ${name}: ${message}`);
}
}
@Component({
selector: 'app-provider-5',
template: template,
providers: [UserService, { provide: Logger, useClass: EvenBetterLogger}]
})
export class Provider5Component {
log: string;
constructor(logger: Logger) {
logger.log('Hello from EvenBetterLogger');
this.log = logger.logs[0];
}
}
////////////////////////////////////////
class NewLogger extends Logger { }
class OldLogger {
logs: string[] = [];
log(message: string) {
throw new Error('Should not call the old logger!');
}
}
@Component({
selector: 'app-provider-6a',
template: template,
providers: [NewLogger,
// not aliased! creates two instances of newlogger
{ provide: OldLogger, useClass: NewLogger }]
})
export class Provider6aComponent {
log: string;
constructor(newLogger: NewLogger, oldLogger: OldLogger) {
if (newLogger === oldLogger) {
throw new Error('expected the two loggers to be different instances');
}
oldLogger.log('Hello OldLogger (but we want NewLogger)');
// the newlogger wasn't called so no logs[] display logs of oldlogger
this.log = newLogger.logs[0] || oldLogger.logs[0];
}
}
@Component({
selector: 'app-provider-6b',
template: template,
providers: [NewLogger,
// alias oldlogger w/reference to newlogger
{ provide: OldLogger, useExisting: NewLogger }]
})
export class Provider6bComponent {
log: string;
constructor(newLogger: NewLogger, oldLogger: OldLogger) {
if (newLogger !== oldLogger) {
throw new Error('expected the two loggers to be the same instance');
}
oldLogger.log('Hello from NewLogger (via aliased OldLogger)');
this.log = newLogger.logs[0];
}
}
////////////////////////////////////////
// an object in shape of logger service
const androidLogger = {
logs: ['My polymer princess, sending you all my love. Provided via "useValue"'],
log: () => { }
};
@Component({
selector: 'app-provider-7',
template: template,
providers: [{ provide: Logger, useValue: androidLogger }]
})
export class Provider7Component {
log: string;
constructor(logger: Logger) {
logger.log('Hello from logger provided with useValue');
this.log = logger.logs[0];
}
}
////////////////////////////////////////
@Component({
selector: 'app-provider-8',
template: template,
providers: [technologyServiceProvider, Logger, UserService]
})
export class Provider8Component {
// must be true of else this component would blow up at runtime
log = 'Technology service injected successfully via technologyServiceProvider';
constructor(technologyService: TechnologyService) { }
}
////////////////////////////////////////
@Component({
selector: 'app-provider-9',
template: template,
providers: [{ provide: APP_CONFIG, useValue: TECHNOLOGY_DI_CONFIG }]
})
export class Provider9Component implements OnInit {
log: string;
constructor(@Inject(APP_CONFIG) private config: AppConfig) { }
ngOnInit() {
this.log = 'APP_CONFIG Application title is ' + this.config.title;
}
}
////////////////////////////////////////
import { Optional } from '@angular/core';
const some_message = 'Hello from injected logger';
@Component({
selector: 'app-provider-10',
template: template,
providers: [{ provide: Logger, useValue: null}]
})
export class Provider10Component implements OnInit {
log: string;
constructor(@Optional() private logger: Logger) {
if (this.logger) {
this.logger.log(some_message);
}
}
ngOnInit() {
this.log = this.logger ? this.logger.logs[0] : 'Optional logger was not available';
}
}
//////////////////////////////////////////
@Component({
selector: 'app-providers',
template: `
<h2>Provider variations</h2>
<div id="p1"><app-provider-1></app-provider-1></div>
<div id="p3"><app-provider-3></app-provider-3></div>
<div id="p4"><app-provider-4></app-provider-4></div>
<div id="p5"><app-provider-5></app-provider-5></div>
<div id="p6a"><app-provider-6a></app-provider-6a></div>
<div id="p6b"><app-provider-6b></app-provider-6b></div>
<div id="7"><app-provider-7></app-provider-7></div>
<div id="8"><app-provider-8></app-provider-8></div>
<div id="9"><app-provider-9></app-provider-9></div>
<div id="10"><app-provider-10></app-provider-10></div>
`
})
export class ProvidersComponent { }
AppComponent:
import { Component } from '@angular/core';
import { Logger } from './logger.service';
import { UserService } from './user.service';
@Component({
selector: 'app-root',
template: `
<div class="container">
<div class="left">
<h1>{{ title }}</h1>
<app-spaceship></app-spaceship>
<app-injectors></app-injectors>
<app-tests></app-tests>
<h2>User</h2>
<p id="user">
{{ userInfo }}
<button (click)="nextUser()">Next User</button>
</p>
<app-my-technologies id="authorized" *ngIf="isAuthorized"></app-my-technologies>
<app-my-technologies id="unauthorized" *ngIf="!isAuthorized"></app-my-technologies>
<app-providers></app-providers>
</div>
<div class="right">
<img class="img-round" [src]="imageUrl" alt="princess image">
</div>
</div>
`
})
export class AppComponent {
imageUrl = '../assets/polymer3.jpg';
title = 'Tour Of Technologies';
constructor(private userService: UserService) { }
get isAuthorized() { return this.user.isAuthorized; }
nextUser() { return this.userService.getNewUser(); }
get user() { return this.userService.user; }
get userInfo() {
return `Current user, ${this.user.name}, is ` +
`${this.isAuthorized ? '' : 'not'} authorized.`;
}
}
Let's view the results in our Chromium Web Browser: