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:

results matching ""

    No results matching ""