Angular 2 DI

Who am I?

{
    "name": "Ilia Idakiev",
    "experience": [
        "Lecturer in 'Advanced JS' @ FMI",
        "Developer / Manager / Owner @ HNS",
        "Project Consultant @ ISS"
    ],
    "involvedIn": [
        "SofiaJS",
        "BeerJS"
    ],
    "latestAchievements
": [
        "Angular 2 Course @ HackBulgaria"
    ]
}

I love vinyl ...

And I like playing vinyl ...

Overview

  • Dependency Injection Pattern
     
  • Providers and Injectors
     
  • Multi-Providers (MP)
     
  • Creating form validations using MP
     
  • How they work
     
  • Other usages of MP

Cohesion

Cohesion measures the strength of relationship between pieces of functionality within a given module / class

Cohesive Modules / Classes

  • Represent a single entity
     
  • They have a well defined role

The goal is high cohesion

Coupling

A measure of how closely connected two modules are.

The goal is loose coupling

Tight vs Loose Coupling

Images downloaded from: https://gamedevelopment.tutsplus.com

function CoffeeMaker() {
    var grinder = new Geinder();
    var pump = Pump.getInstance();
    var heater = app.get('Heater');
    this.brew = function() {
        grinder.grind();
        heater.on();
        pump.pump(); 
    }
}

Tight coupling

  • A change in one module usually forces changes in other modules.
     
  • Assembly of modules might require more effort and time due to the increased inter-module dependency.
     
  • A particular module might be harder to reuse and test because dependent modules must be included.

S.O.L.I.D.

S.O.L.I.D

  1. Single Responsibility Principle
     
  2. Open/Closed Principle
     
  3. Liskov Substitution Principle
     
  4. Interface Segregation Principle
     
  5. Dependency Inversion Principle

Dependency Inversion Principle

Dependency Inversion Principle

  • High-level modules should not depend on low-level modules. Both should depend on abstractions.
     
  • Abstractions should not depend upon details. Details should depend upon abstractions.

Dependency Injection

It's a coding pattern in which a class receives its dependencies from external sources rather than creating them itself.

Dependency Injection    (Design Pattern)

function CoffeeMaker(grinder, pump, heater) {
    this.brew = function() {
        grinder.grind();
        heater.on();
        pump.pump();
    }
}

Easy to reuse in different environments => Easy Testing

Using DI Pattern

(wiring)

function main() {
    var electricity = new Electricity();
    var grinder = new Grinder(electricity);
    var heater = new Heater(electricity);
    var pump = new Pump(heater, electricity);
    var coffeeMaker = new CoffeeMaker(grinder, pump, heater);

    coffeeMaker.brew();
}

Using DI Pattern

(maintenance)

function main() {
    var logger = new Logger();
    var electricity = new Electricity(logger);
    var grinder = new Grinder(electricity, logger);
    var heater = new Heater(electricity, logger);
    var pump = new Pump(heater, electricity, logger);
    var coffeeMaker = new CoffeeMaker(grinder, pump, heater, logger);

    coffeeMaker.brew();
}

We have to update everywhere we want to use the logger and all of our tests.

DI Container

(no maintenance)

function main() {
    var injector = new Injector(...);
    var coffeeMaker = injector.get(CoffeeMaker);

    coffeeMaker.brew();
}

We don't have information about the dependencies anymore so we don't need to maintain main function.

Angular 2 DI

  • Provider - The provider knows how to create the objects. It takes a token and maps it to a factory function that creates the object.
     
  • Injector - The injector relies on the providers to create the instances of the dependencies our components need.

Providers

  • Providers can be defined in a ngModule or inside a Component.
     
  • Providers registered in the ngModule will be available to all components within the module.
     
  • Providers registered within the Component will be available only to the Component and its children.

Injectors

  • Each Component has its own Injector.
     
  • A provider lookup is performed on the parent Injector if a provider is not found.
    (When a Component is set as host the lookup will end there)

Creating an Injector

var resolvedProviders = Injector.resolve([
    { 
        provide: 'message', 
        useValue: 'Hello' 
    }
]);

var injector = Injector.fromResolvedProviders(resolvedProviders);

expect(injector.get('message')).toEqual('Hello');

Injector.resolve returns an array of resolvedReflectiveProviders

DI with Injector

import {Injector, Injectable} from 'angular2/angular2';

@Injectable()
class Engine {
}
 
@Injectable()
class Car {
  constructor(public engine:Engine) {}
}

var injector = Injector.resolveAndCreate([Car, Engine]);
var car = injector.get(Car);

expect(car instanceof Car).toBe(true);
expect(car.engine instanceof Engine).toBe(true);

Resolution - flatterning multiple nested arrays and converting individual providers to resolvedReflectiveProviders

Decorators

Decorators provide a way to add both annotations and a meta-programming syntax for class declarations and members. Decorators are a stage 1 proposal for JavaScript and are available as an experimental feature of TypeScript (+ reflect-metadata library).

Decorators

A Decorator can be attached to a class declaration, method, accessor, property, or parameter. Decorators use the form @expression, where expression must evaluate to a function that will be called at runtime with information about the decorated declaration.

Simple decorator

function memoize(target: Object, key: string, descriptor: TypedPropertyDescriptor<any>) {
    var hash = {}; 
    var originalMethod = descriptor.value;
    descriptor.value = function (...args) {
        var str = JSON.stringify(args), data;
        if (data = hash[str]) return data;
        console.log('calculating ...');
        hash[str] = (data = originalMethod(...args));
        return data;
    };
}

class Util { 

    @memoize
    add(a, b) { 
        return a + b;
    }

}

var util = new Util();
console.log(util.add(1, 2));
console.log(util.add(1, 2));

Higher order functions

These are functions that:

  • take one or more functions as arguments
     
  • as result they return a function

Decorator factory

class MyClass {
    @enumerable(false)
    get prop() {
        return true;
    }
    prop2 = 'str';
}

function enumerable(isEnumerable: boolean) {
    return (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) => {
        descriptor.enumerable = isEnumerable;
        return descriptor;
    };
}

var myClass = new MyClass();
console.log(Object.keys(myClass)); // > ['prop2'];

Some Angular 2 Decorators

@NgModule

( decorator )

export interface NgModuleDecorator {
  /*Defines the set of injectable objects that are available in the injector of this module.*/
  providers?: Provider[];
  /*Specifies a list of directives/pipes that belong to this module.*/
  declarations?: Array<Type<any>|any[]>;
  /*Specifies a list of modules whose exported directives/pipes
   *should be available to templates in this module.*/
  imports?: Array<Type<any>|ModuleWithProviders|any[]>;
  ...
  /*Specifies a list of components that should be compiled when this module is defined.
   *For each component listed here, Angular will create a {@link ComponentFactory}
   *and store it in the {@link ComponentFactoryResolver}.*/
  entryComponents?: Array<Type<any>|any[]>;
  /*Defines the components that should be bootstrapped when
   *this module is bootstrapped. The components listed here
   *will automatically be added to `entryComponents`.*/
  bootstrap?: Array<Type<any>|any[]>;
  ...
}

Copied from angular @ GitHub
(modified to fit on screen)

@Component

( decorator )

export interface Component extends Directive {
  /* * **animations** - list of animations of this component
   * * **changeDetection** - change detection strategy used by this component
   * * **encapsulation** - style encapsulation strategy used by this component
   * * **entryComponents** - list of components that are dynamically inserted into the view of this
   *   component
   * * **exportAs** - name under which the component instance is exported in a template
   * * **host** - map of class property to host element bindings for events, properties and
   *   attributes
   * * **inputs** - list of class property names to data-bind as component inputs
   * * **interpolation** - custom interpolation markers used in this component's template
   * * **moduleId** - ES/CommonJS module id of the file in which this component is defined
   * * **outputs** - list of class property names that expose output events that others can
   *   subscribe to
   * * **providers** - list of providers available to this component and its children
   * * **queries** -  configure queries that can be injected into the component
   * * **selector** - css selector that identifies this component in a template
   * * **styleUrls** - list of urls to stylesheets to be applied to this component's view
   * * **styles** - inline-defined styles to be applied to this component's view
   * * **template** - inline-defined template for the view
   * * **templateUrl** - url to an external file containing a template for the view
   * * **viewProviders** - list of providers available to this component and its view children*/
}

Copied from angular @ GitHub
(modified to fit on screen)

@Inject

( decorator )

/**
 * Type of the Inject decorator / constructor function.
 * @stable
 */
export interface InjectDecorator {
  /**
   * @whatItDoes A parameter decorator that specifies a dependency.
   * @howToUse
   * ```
   * @Injectable()
   * class Car {
   *   constructor(@Inject("MyEngine") public engine:Engine) {}
   * }
   * ```
   * When `@Inject()` is not present, {@link Injector} will use the type annotation of the
   * parameter.
   */
  (token: any): any;
  new (token: any): Inject;
}

Copied from angular @ GitHub
(modified to fit on screen)

Angular 2 Component DI

( providing )

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';

import { AppComponent } from './app.component';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule
  ],
  providers: [
    { 
      provide: 'siteUrl', 
      useValue: 'http://localhost:8080/users'
    }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Angular 2 DI

( injecting and using )

import { Component, Inject } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  url:string;
  constructor(@Inject('siteUrl') url) {
    this.url = url;
  }
}

Providing a class

( using type annotations )

import { Component, Inject } from '@angular/core';

class MyClass {
  name:string;
  constructor() {
    this.name = 'MyClass';
  }
  getClassName() {
    return this.name;
  }
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [{ provide: MyClass, useClass: MyClass }]
})
export class AppComponent {
  url:string;
  constructor(myClass: MyClass) {
    console.log(myClass.getClassName());
  }
}

Providing a class (short way)

( using type annotations )

import { Component, Inject } from '@angular/core';

class MyClass {
  name:string;
  constructor() {
    this.name = 'MyClass';
  }
  getClassName() {
    return this.name;
  }
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [ MyClass ]
})
export class AppComponent {
  url:string;
  constructor(myClass: MyClass) {
    console.log(myClass.getClassName());
  }
}

Alternative class provider

( using annotations )

import { Component, Inject } from '@angular/core';

class MyOtherClass {
  name:string;
  constructor() {
    this.name = 'MyOtherClass';
  }
  getClassName() {
    return this.name;
  }
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [{ provide: MyClass, useClass: MyOtherClass }]
})
export class AppComponent {
  url:string;
  constructor(myClass: MyClass) {
    console.log(myClass.getClassName());
  }
}

Aliased class providers

( using annotations )

@Component({
  ...
  providers: [ NewLogger,
  // Alias OldLogger w/ reference to NewLogger
  { provide: OldLogger, useExisting: NewLogger }]
})
export class AppComponent {
  ...
}

Suppose an old component depends upon an OldLogger class. OldLogger has the same interface as the NewLogger, but for some reason we can't update the old component to use it.

For more provider configurations
 

Components vs Directives

  • Component = Directive + view (template)
     
  • Components have an Injector
     
  • Directives use the Injector of their parent Component 

Handy stuff that we can use for DI

  • OpaqueToken
     
  • forwardRef

OpaqueToken

( Creates a token that can be used in a DI Provider )

/**
 * Creates a token that can be used in a DI Provider.
 * ```typescript
 * var t = new OpaqueToken("value");
 * var injector = Injector.resolveAndCreate([
 *   {provide: t, useValue: "bindingValue"}
 * ]);
 * expect(injector.get(t)).toEqual("bindingValue");
 * ```
 * Using an `OpaqueToken` is preferable to using strings as tokens 
 * because of possible collisions caused by multiple providers using 
 * the same string as two different tokens. Using an `OpaqueToken` is 
 * preferable to using an `Object` as tokens because it provides better
 * error messages.
 * @stable
 */
@Injectable()  // so that metadata is gathered for this class
export class OpaqueToken {
  constructor(private _desc: string) {}
  toString(): string { return `Token ${this._desc}`; }
}

Copied from angular @ GitHub
(modified to fit on screen)

forwardRef

( allows to refer to references which are not yet defined )

/**
 * Allows to refer to references which are not yet defined.
 *
 * For instance, `forwardRef` is used when the `token` which
 * we need to refer to for the purposes of DI is declared,
 * but not yet defined. It is also used when the `token` 
 * which we use when creating a query is not yet defined.
 *
 * ### Example
 * {@example core/di/ts/forward_ref/forward_ref_spec.ts region='forward_ref'}
 * @experimental
 */
export function forwardRef(forwardRefFn: ForwardRefFn): Type<any> {
  (<any>forwardRefFn).__forward_ref__ = forwardRef;
  (<any>forwardRefFn).toString = function() { return stringify(this()); };
  return (<Type<any>><any>forwardRefFn);
}

Copied from angular @ GitHub
(modified to fit on screen)

Multi Providers

Multi Providers

Multi providers extend the thing that is being injected for a particular token.

import { Component, Inject, OpaqueToken } from '@angular/core';

var URLs = new OpaqueToken('URLs');

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  providers: [
    { provide: URLs, useValue: 'http://google.com/', multi: true },
    { provide: URLs, useValue: 'http://yahoo.com/', multi: true }
  ]
})
export class AppComponent {
  url:string;
  constructor(@Inject(URLs) urls:string[]) {
    this.url = urls.join(',');
  }
}

Multi Providers

Create token

import { OpaqueToken } from '@angular/core';

export const MY_DIRECTIVES = new OpaqueToken('my_directives');

app/shared/my_directives/token.ts

Multi Providers

Providing directives (1)

import { Directive, forwardRef } from '@angular/core';
import { MY_DIRECTIVES } from './token';

let direct1Provider = {
  provide: MY_DIRECTIVES,
  useExisting: forwardRef(() => Direct1Directive),
  multi: true
};

@Directive({
  selector: '[direct1]',
  providers: [direct1Provider]
}) export class Direct1Directive {
  doSomething() { return false; }
}

app/shared/my_directives/direct1.directive.ts

Multi Providers

Providing directives (2)

import { Directive, forwardRef } from '@angular/core';
import { MY_DIRECTIVES } from './token';

let direct2Provider = {
  provide: MY_DIRECTIVES,
  useExisting: forwardRef(() => Direct2Directive),
  multi: true
};

@Directive({
  selector: '[direct2]',
  providers: [direct2Provider]
}) export class Direct2Directive {
  doSomething() { return true; }
}

app/shared/my_directives/direct2.directive.ts

Multi Providers

The collector directive

import { Directive, forwardRef, Inject } from '@angular/core';
import { MY_DIRECTIVES } from './token';

let collectorProvider = {
  provide: CollectorDirective,
  useExisting: forwardRef(() => CollectorDirective)
};


@Directive({
  selector: '[collector]',
  providers: [collectorProvider]
}) export class CollectorDirective {
  constructor(@Inject(MY_DIRECTIVES) private _directs:any[]) {
    console.log('collector', _directs);
  }
}

app/shared/my_directives/collector.directive.ts

Multi Providers

The app component

import { Component } from '@angular/core'
import { DirectivesModule } from './shared/my_directives/directives.module';

@Component({
  selector: 'my-app',
  template: `
    <div>
      <div direct1 direct2 collector></div>
    </div>
  `,
}) 
export class App {
  constructor() {}
}

app/app.component.ts

Result

from console.log

Multi Providers

The container

import { Directive, forwardRef } from '@angular/core';
import { CollectorDirective } from './collector.directive';
import { MY_DIRECTIVES } from './token';

let containerDirectiveProvider = {
  provide: ContainerDirective,
  useExisting: forwardRef(() => ContainerDirective),
};

@Directive({
  selector: '[container]',
  providers: [containerDirectiveProvider]
}) export class ContainerDirective {
  private collectors: CollectorDirective[] = [];
  
  addCollector(collector:CollectorDirective) {
    this.collectors.push(collector);
  }

  ngOnChange() {
    var result = this.collectors.map(col => col.doSomething());
  }
}

app/shared/my_directives/container.directive

Multi Providers

The app controller

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
  <div container>
    <div collector direct1 direct2></div>
  </div>
  `
})
export class App {

}

app/shared/my_directives/container.directive

Multi Providers

collector directive modification

import { Directive, forwardRef, Inject } from '@angular/core';
import { MY_DIRECTIVES } from './token';
import { ContainerDirective } from './container.directive';

let collectorProvider = {
  provide: CollectorDirective,
  useExisting: forwardRef(() => CollectorDirective)
};

@Directive({
  selector: '[collector]',
  providers: [collectorProvider]
}) export class CollectorDirective {
  constructor(@Inject(MY_DIRECTIVES) public _directs:any[], 
              private _parent:ContainerDirective) {
    this._parent.addCollector(this);
  }
  doSomething() {
    var result = this._directs.map(direct => direct.doSomething());
    return results;
  }
}

app/shared/my_directives/collector.directive

Demo

Form Validation

import { Directive, forwardRef, Input } from '@angular/core';
import { NG_VALIDATORS, ValidatorFn, FormControl } from '@angular/forms';

var minAmountFnFactory = amount => 
  c => c.value > amount ? null : { valid: false };

var minAmountProvider = {
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => MinAmountDirective),
  multi: true
}

@Directive({
  selector: '[minAmount][ngModel]',
  providers: [ minAmountProvider ]
}) export class MinAmountDirective {
  private _validator:ValidatorFn;
  
  @Input() set minAmount(value:number) {
    this._validator = minAmountFnFactory(value);
  }
  
  validate(c:FormControl) {
    return this._validator(c);
  }
}

Form Validation

app component

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
  <form>
    <input type="number" [minAmount]="200" 
        [(ngModel)]="amount" #name="ngModel" name="amount">
    <div *ngIf="!name.valid">Amount must be > 200</div>
  </form>
  `,
})
export class App {
  amount:number = 0;
}

Async Form Validation

import { Directive, forwardRef } from '@angular/core';
import { NG_ASYNC_VALIDATORS, ValidationResult } from '@angular/forms';

let emailValidatorProvider = {
  provide: NG_ASYNC_VALIDATORS, 
  useExisting: forwardRef(() => EmailValidator),
  multi: true
};

@Directive({
    selector: '[emailValidator][ngModel]',
    providers: [ emailValidatorProvider ]
})
export class EmailValidator {
    constructor(private _userService:UserService) { }
    validate(control:Control):Promise<ValidationResult> {
        return new Promise((resolve, reject) => {
            this._userService.exists(control.value).subscribe((response:any) => {
                    if(response.exists) return resolve({ userExists: { valid: false } });
                    else return resolve(null);
                }, (error: any) => { console.log(error); })
        });
    }
}

Custom Form Control

import { Component } from '@angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';

let customControl = { 
  provide: NG_VALUE_ACCESSOR,
  useExisting: forwardRef(() => MyCustomFormControl),
  multi: true
}

@Component({
    ...
}) export class MyCustomFormControl implements ControlValueAccessor {
  ...
  writeValue(obj: any) {
    //write value to our custom element
  }
  registerOnChange(fn:any) {
    this.propagateChange = fn; 
    //fn is a function that informs the outside world about changes
  }
  registerOnTouched(fn:any) { 
    this.touchedCallback = fn;
    //fn a callback that is excuted whenever a form control is “touched”
  }
  setDisabledState(isDisabled: boolean) {
    this.isDisabled = isDisabled;
  }
}

Questions

Follow

https://github.com/iliaidakiev

Made with Slides.com