Angular

Angular, overview

Angular, installation

Angular, installation customization

Angular, configuration

Angular, dev., test and execution

Angular, notion of “module” and “component”

Angular, “root” module

// 'main.ts' file
…
import {My_root_module} from './app/app.module';
…
platformBrowserDynamic().bootstrapModule(My_root_module).catch(err => window.console.warn(err));
// 'app.module.ts' file
import {NgModule} from '@angular/core';
// Angular base libraries:
import {BrowserModule} from '@angular/platform-browser';
// Homemade ("feature") module (JavaScript-based import):
import {Currencies_module} from '../currencies/currencies.module';

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

@NgModule({ // Module declaration thanks to TypeScript decorator
    declarations: [AppComponent /* Component as part of the module... */],
    imports: [BrowserModule,
        // Homemade ("feature") module (Angular-based import):
        Currencies_module],
    providers: [],
    bootstrap: [AppComponent]
})
export class My_root_module {}

Angular, “feature” module

// 'currencies.module.ts' file
import {NgModule} from '@angular/core'; // TypeScript decorator
import {CommonModule} from '@angular/common';
import {ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';

import {Currencies} from './currencies.component';
import {Currencies_controller} from './currencies-controller.component';
import {Currencies_information} from './currencies-information.component';
import {Currencies_menu} from './currencies-menu.component';
import {Currencies_service} from './currencies-service';
import {Currencies_wrapper} from './currencies-wrapper.component';

@NgModule({ // TypeScript decorator
    // These components belong to *only one* module:
    declarations: [Currencies, Currencies_controller, Currencies_menu, Currencies_information, Currencies_wrapper],
    imports: [ CommonModule, ReactiveFormsModule, RouterModule.forRoot(Currencies_menu.Navigations) ],
    exports: [
        // Currencies, // No needed export for use in 'Currencies_wrapper': both components belong to the same module...
        Currencies_menu,
        // Currencies_wrapper, // Because of routing, 'currencies-wrapper' tag is no longer used in 'app.component.html'...
        RouterModule // To let access to the navigations at the app. level...
    ] // '@Injectable({providedIn: 'root'})' instead:
    // providers: [Currencies_service] // When you register a provider with a specific 'NgModule', the same instance of a service is available to all components in that 'NgModule'
})
export class Currencies_module {}

Angular, user-defined (empty) component

// 'my-component.component.ts' file
import {Component, OnDestroy, OnInit} from '@angular/core';
@Component({
  selector: 'my-component', // HTML tag
  templateUrl: './my-component.component.html',
  styleUrls: ['./my-component.component.css']
})
export class My_component implements OnDestroy, OnInit {
// Possible constructor parameters mean "dependency injection":
  constructor() {}
// Lifecycle hook -> synchronization with Angular:
  ngOnDestroy(): void {this._service.close(1000, 'Normal Closure');}
// Lifecycle hook -> synchronization with Angular:
  ngOnInit(): void {this._service = new WebSocket("ws://localhost:1963/FranckBarbier/WebSockets_illustration");}
}

Angular, homemade component

// currencies-wrapper.component.ts file
import {Component} from '@angular/core';
import {Currency} from './currencies.component'; // JavaScript 'import'

@Component({ // TypeScript decorator
    selector: 'currencies-wrapper',
    template: `
        <div>
            <h2>Currency of interest: {{read()}}</h2>
            <!-- 'Currencies_wrapper' as a wrapper of 'Currencies' -->
            <!-- Data binding from parent (wrapper) to child: 'currency_of_interest_wrapper' getter is used -->
            <!-- Data binding from child to parent (wrapper): '_send_currency' received event -->
            <currencies [currency_of_interest]="currency_of_interest_wrapper" (_send_currency)="update($event)"></currencies>
        </div>
    `,
    styles: ['div { border: 5px dashed yellow; font-weight: normal; }', 'h2 { border: 5px dashed lightgreen; }']
})
export class Currencies_wrapper {
    private _currency_of_interest_wrapper: Currency = null;
    get currency_of_interest_wrapper(): Currency { return this._currency_of_interest_wrapper; }
    public read(): string { return this._currency_of_interest_wrapper !== null ? this._currency_of_interest_wrapper.common_symbol : ""; }
// It must be 'public' to be accessed as the event handler of '_send_currency':
    public update(currency: Currency): void { this._currency_of_interest_wrapper = currency; }
}

Angular, currencies-wrapper rendering

Angular, template syntax (data binding)

Angular template syntax (here…) extends HTML (and JavaScript) so that views reflect data in components. Property binding is when properties (a.k.a. attributes) of HTML tags (a.k.a. elements) like, for instance, id, get values from component instances. As a complement, interpolation is the use of double braces. Expressions support a subset of JavaScript syntax.

<!-- <input type="radio" [id]="currency_.iso_code" [checked]="currency_.common_symbol === '$'"> -->
<input type="radio" [id]="currency_.iso_code" [checked]="checked(currency_)">
<label [for]="currency_.iso_code">{{currency_.common_symbol}}</label>
<nav *ngFor="let navigation of navigations">
    <a routerLink="{{navigation.path.replace(':iso_code','')}}{{navigation.data.iso_code}}" class="button fancy-button">
        <i class="currencies material-icons">{{navigation.data.material_icon}}</i>
    </a>
</nav>

Angular, ngFor and ngIf structural directives (see also here…)

Contrary to attribute directives that change appearance or behavior, structural directives change the DOM structure.

<span *ngFor="let currency_ of currencies"> <!-- Structural directive '*ngFor' -->
    <span *ngIf="currency !== currency_">
        <input type="radio" [id]="currency_.iso_code" [checked]="checked(currency_)">
        <label [for]="currency_.iso_code">{{currency_.common_symbol}}</label>
    </span>
</span>

Angular, ng-template structural directive (see also here…)

ng-template has no DOM counterpart, i.e., it does not yield something visible unless “instantiated”.

<!-- 'currencies-menu.component.html' file -->
<nav class="currencies" *ngFor="let navigation of navigations">
    <a *ngIf="navigation.hasOwnProperty('data') && navigation.data.hasOwnProperty('iso_code'); else no_parameter"
        routerLink="{{navigation.data.iso_code}}" class="button fancy-button">
        <i class="currencies material-icons">{{navigation.data.material_icon}}</i>
    </a>
    <ng-template #no_parameter>
        <a *ngIf="navigation.hasOwnProperty('data');"
            routerLink="{{navigation.path}}" class="button fancy-button">
            <i class="currencies material-icons">{{navigation.data.material_icon}}</i>
        </a>
    </ng-template>
</nav>

Angular, managing CSS

<a on-mouseover="send(currency)" [type]="currency.common_name" 
    [ngStyle]="{'color': checked(currency) ? 'green' : 'red'}">
    {{currency.common_name}}
</a>

Angular, template reference variable (see also here…)

One may notice that ngModel directive has close power to template reference variable: here

// 'currencies-information.component.ts' file
import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core';
…
@Component({
    selector: 'currencies-information',
    template: `
        <div>
            <h2 #my_h2 [ngClass]="{'blue': true, 'red': information().includes(default_substitute_common_symbol)}">{{information() | uppercase}}</h2>
            <span class="material-icons">{{"?" | guess_material_icon: this.material_icon}}</span>
            <hr><button (click)="back()">Back (leave from '{{this.location.path()}}')</button>
        </div>
    `,
    styles: ['div{border: 5px dashed purple;}', '.blue{color: blue}', '.red{color: red}'] // https://angular.io/guide/component-styles ('--inline-style')
})
export class Currencies_information implements AfterViewInit, OnInit {
    @ViewChild('my_h2') // Injection...
    private _my_h2: ElementRef; // https://www.techiediaries.com/angular-elementref/
    ngAfterViewInit() {
        window.console.log(this._my_h2.nativeElement.innerText); // 'nativeElement' <=> 'current' in React
    }
    … // Other features here: 'back()', 'default_substitute_common_symbol', 'information()'...
}

Angular, DOM event capture

DOM events are captured for immediate modification on data in the view's manager component either for immediate or deferred processing.

<!-- 'currencies.component.html' file -->
<!-- 'currencies' is an instance attribute of the 'Currencies' class: -->
<ul *ngFor="let currency of currencies"> <!-- Structural directive '*ngFor' -->
    <li on-mouseover="send(currency)" title={{currency.common_symbol}}
        [ngStyle]="{'color': checked(currency) ? 'green' : 'red'}"> 
        {{currency.common_name}} <!-- Interpolation, i.e., {{ }}, renders a property's value as text -->
    </li>
    (description: 
    <span *ngIf="currency.description">
        {{ currency.description }})
    </span>
    <!-- If the event is a native DOM element event then '$event' is a DOM element object with standard properties like 'target': -->
    <input *ngIf="!currency.description" (change)="currency.description=$event.target.value"
           placeholder="Enter some text" type="text"/>
    )
    <span class="buttons">
      <!-- '(click)' <=> 'on-click' -->
      <button (click)="exchange_rate(currency, currency_of_interest)" mat-fab>Exchange rate</button>
    </span>
    …

Angular, component interaction -parent to child- (see also here…)

<div>
    <h2>Currency of interest: {{read()}}</h2>
    <!-- 'Currencies_wrapper' as a wrapper of 'Currencies' -->
    <!-- Data binding from parent (wrapper) to child: 'currency_of_interest_wrapper' getter is used -->
    <!-- Data binding from child to parent (wrapper): '_send_currency' received event -->
    <currencies [currency_of_interest]="currency_of_interest_wrapper"
        (_send_currency)="update($event)">
    </currencies>
</div>
// 'currencies.component.ts' file
…
@Component({
  selector: 'currencies',
  // providers: [My_stateful_service], // Scope is component only...
  templateUrl: './currencies.component.html',
  styleUrls: ['./currencies.component.css']
})
export class Currencies implements OnChanges, OnDestroy, OnInit {      
    …
    @Input() // Data binding from parent (currencies-wrapper) to child (currencies)...
    set currency_of_interest(currency_of_interest: Currency | null) /* : void */ { // TypeScript setter rejects returned type...
        this._currency_of_interest = currency_of_interest;
    }
    …
}

Angular, component interaction -child to parent, sender- (see also here…)

<!-- 'currencies.component.html' file -->
<ul *ngFor="let currency of currencies"> <!-- 'currencies' is an instance attribute of the 'Currencies' class: -->
    <!-- Tag property binding, i.e., [] -->
    <li on-mouseover="send(currency)" title={{currency.common_symbol}}
        [ngStyle]="{'color': checked(currency) ? 'green' : 'red'}">
        <!-- Interpolation, i.e., {{ }}, renders a property's value as text: -->
        {{currency.common_name}}
    </li>
    …
import {Component, EventEmitter, …, Output, …} from '@angular/core';
…
export class Currencies … {
    …
    // Parent listens for child event
    @Output() // Mandatory!
    private readonly _send_currency = new EventEmitter<Currency>();
    public send(currency: Currency): void {
        // window.alert('emit: ' + currency.common_symbol);
        this._send_currency.emit(currency);
    }
}

Angular, component interaction -child to parent, handler-

// currencies-wrapper.component.ts file
…
@Component({ // TypeScript decorator
    selector: 'currencies-wrapper',
    template: `
        <div>
            <h2>Currency of interest: {{read()}}</h2>
            <!-- 'Currencies_wrapper' as a wrapper of 'Currencies' -->
            <!-- Data binding from parent (wrapper) to child: 'currency_of_interest_wrapper' getter is used -->
            <!-- Data binding from child to parent (wrapper): '_send_currency' received event -->
            <currencies [currency_of_interest]="currency_of_interest_wrapper" (_send_currency)="update($event)"></currencies>
        </div>
    `,
    styles: ['div { border: 5px dashed yellow; font-weight: normal; }', 'h2 { border: 5px dashed lightgreen; }']
})
export class Currencies_wrapper {
    private _currency_of_interest_wrapper: Currency = null;
    get currency_of_interest_wrapper(): Currency { return this._currency_of_interest_wrapper; }
    public read(): string { return this._currency_of_interest_wrapper !== null ? this._currency_of_interest_wrapper.common_symbol : ""; }
// It must be 'public' to be accessed as the event handler of '_send_currency':
    public update(currency: Currency): void { this._currency_of_interest_wrapper = currency; }
}

Angular, component lifecycle hooks (see also here… and there…)

As a “framework”, Angular instantiates components for us according to views' evolution. To that extent, dependency injection is the delivery of facilities (for instance, services) to process data inside components. Key steps like in-view changes lead Angular to call predefined (public) functions to synchronize works assigned to Angular and components. These functions are called hooks.

Angular, component lifecycle hooks (OnChanges, sender)

// currencies-wrapper.component.ts file
…
@Component({ // TypeScript decorator
    selector: 'currencies-wrapper',
    template: `
        <div>
            <h2>Currency of interest: {{read()}}</h2>
            <!-- 'Currencies_wrapper' as a wrapper of 'Currencies' -->
            <!-- Data binding from parent (wrapper) to child: 'currency_of_interest_wrapper' getter is used -->
            <!-- Data binding from child to parent (wrapper): '_send_currency' received event -->
            <currencies [currency_of_interest]="currency_of_interest_wrapper" (_send_currency)="update($event)"></currencies>
        </div>
    `,
    styles: ['div { border: 5px dashed yellow; font-weight: normal; }', 'h2 { border: 5px dashed lightgreen; }']
})
export class Currencies_wrapper {
    private _currency_of_interest_wrapper: Currency = null;
    get currency_of_interest_wrapper(): Currency { return this._currency_of_interest_wrapper; }
    public read(): string { return this._currency_of_interest_wrapper !== null ? this._currency_of_interest_wrapper.common_symbol : ""; }
// It must be 'public' to be accessed as the event receiver of '_send_currency':
    public update(currency: Currency): void { this._currency_of_interest_wrapper = currency; }
}

Angular, component lifecycle hooks (OnChanges, receiver)

// 'currencies.component.ts' file
// tslint:disable
import {Component, EventEmitter, OnChanges, OnDestroy, OnInit, Input, Output, SimpleChanges} from '@angular/core';
…
export class Currencies implements OnChanges, OnDestroy, OnInit {
    …
    ngOnChanges(changes: SimpleChanges): void {
        if (changes.hasOwnProperty('currency_of_interest') && changes.currency_of_interest) 
            window.console.warn("'ngOnChanges': " + JSON.stringify(changes));
    }
    …

Angular, routing (“Currencies” nav.)

Angular offers support for managing navigation that leads to view changes in a Single Page Application (SPA) logic (further detail here…).

Angular, routing (“Dollar” nav.)

Angular, routing (“New” nav.)

Angular, routing (configuration)

// 'currencies.module.ts' file
import {NgModule} from '@angular/core'; // TypeScript decorator
import {CommonModule} from '@angular/common';
import {ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';

import {Currencies} from './currencies.component';
import {Currencies_controller} from './currencies-controller.component';
import {Currencies_information} from './currencies-information.component';
import {Currencies_menu} from './currencies-menu.component';
import {Currencies_wrapper} from './currencies-wrapper.component';

@NgModule({ // TypeScript decorator
  // These components belong to *only one* module:
  declarations: [Currencies, Currencies_controller, Currencies_information, Currencies_menu, Currencies_wrapper],
  imports: [CommonModule, ReactiveFormsModule, RouterModule.forRoot(Currencies_menu.Navigations)],
  exports: [
    // Currencies, // No needed export for use in 'Currencies_wrapper': both components belong to the same module...
    Currencies_menu, // For App.
    // Currencies_wrapper, // Because of routing, 'currencies-wrapper' tag is no longer used in 'app.component.html'...
    RouterModule // To let access to navigation at the app. level...
  ]
})
export class Currencies_module {}

Angular, routing (configuration cont'd)

<!-- 'app.component.html' file -->
<!-- 'currencies-menu' is defined in './currencies/currencies-menu.component.ts' file: -->
<currencies-menu></currencies-menu> <!-- 'My_root_module' must import 'Currencies_module' (Angular style) -->
<!-- Directive is accessible through export of 'RouterModule' in 'Currencies_module' (Angular style): -->
<router-outlet></router-outlet> <!-- It replaces 'currencies-wrapper': -->
<!-- <currencies-wrapper></currencies-wrapper> -->

Angular, routing (navigation)

<!-- 'currencies-menu.component.html' file -->
<nav class="currencies" *ngFor="let navigation of navigations">
    <a *ngIf="navigation.hasOwnProperty('data') && navigation.data.hasOwnProperty('iso_code'); else no_parameter"
        routerLink="{{navigation.data.iso_code}}" class="button fancy-button">
        <i class="currencies material-icons">{{navigation.data.material_icon}}</i>
    </a>
    <ng-template #no_parameter>
        <a *ngIf="navigation.hasOwnProperty('data');"
            routerLink="{{navigation.path}}" class="button fancy-button">
            <i class="currencies material-icons">{{navigation.data.material_icon}}</i>
        </a>
    </ng-template>
</nav>

Angular, routing (routes)

// 'currencies-menu.component.ts' file
…
export class Currencies_menu {
    public static readonly Navigations: Routes = [
        { path: 'Currencies', // No slash, i.e., '/Currencies'
            component: Currencies_wrapper,
            data: {material_icon: 'payment'} // https://material.io/resources/icons/?style=baseline
        },
        { path: 'New',
            component: Currencies_controller,
            data: {material_icon: 'fiber_new'}
        },
        { path: ':iso_code', // Route parameter
            component: Currencies_information,
            data: {iso_code: Currencies.Currencies[Currencies.Dollar].iso_code,
                material_icon: Currencies.Currencies[Currencies.Dollar].material_icon}
        },
        { path: ':iso_code', // Route parameter
            component: Currencies_information,
            data: {iso_code: Currencies.Currencies[Currencies.Euro].iso_code,
                material_icon: Currencies.Currencies[Currencies.Euro].material_icon}
        },
        {path: '', redirectTo: '/Currencies', pathMatch: 'full'} // As default...
        // , {path: '**', component: null} /* The router selects this route if the requested URL doesn't match any paths for routes defined earlier in the configuration (caution: no handler yet!) */
    ];
    …
}

Angular, routing (router state)

// 'currencies-information.component.ts' file
…
export class Currencies_information implements AfterViewInit, OnInit {
    …
    private _information: string = "";
    constructor(readonly location: Location, private readonly _navigation: ActivatedRoute) { // Dependency injection
        // Don't immediately use 'location' and '_navigation'...
    }
    public back(): void { this.location.back(); }
    public information(): string { return "Here: " + this._information; }
    ngOnInit() {
        _navigation.url.subscribe(() => { // Any time URL changes, this callback is fired...
            const currency = Currencies.Currencies.find(currency => {
                return "/" + `${currency.iso_code}` === this._navigation['_routerState'].snapshot.url;
            });
            this._information = _navigation.snapshot.toString() + " - " + currency.common_symbol;
        });
        this._navigation.paramMap.subscribe((parameters: ParamMap) => {
            const iso_code = parameters.get("iso_code"); // '840' for Dollar, etc.
        });
    }
}

Angular, routing (navigation logic)

Apart from using the action attribute of the form tag (e.g., pushing form data to an external URL), Angular offers native support for implementing navigation logic.

// 'currencies-menu.component.ts' file
…
export class Currencies_menu {
    …
    // Navigation logic (child-to-child communication based on injected service):
    constructor(private readonly _change_route_service: Change_route_service, private readonly _router: Router) {}
    ngOnInit() {
        this._change_route_service.receive_path((path: string) => {
            this._router.navigate([path]);
        });
    }
}

Angular, routing (navigation logic cont'd)

// 'currencies-controller.component.ts' file
…
export class Currencies_controller implements OnInit {
    …
    constructor(private readonly _change_route_service: Change_route_service) {} // Dependency injection
    …
    public record_currency(): void {
        /* Add new data in 'Currencies.Currencies'... */
        /* Add new corresponding route in 'Currencies_menu.Navigations'... */
        const next_round = Math.floor(Math.random() * 2); // For the fun: '0' or '1'...
        if (next_round === 0) {
            …
            this.new_currency.reset(); // Next round...
        } else {
            …
            this._change_route_service.send_path('Currencies'); // Next round...
        }
    }
    …
}

Angular, notion of “service”

Angular, service and injectability

Service creation: ng g service ../currencies/currencies-service --flat.

// 'currencies-service.ts' file
import {Injectable} from '@angular/core';
…
@Injectable({providedIn: 'root'  /* 'root' injector -> Angular creates a single, shared instance... */})
export class Currencies_service {
    …
    private _from: Currency;
    private _to: Currency;
    private readonly _request = new XMLHttpRequest();
    // Constructor here...
    public exchange_rate_(from: Currency, to: Currency): void { // Free!
        this._from = from;
        this._to = to;
        // Stupid, request can be run once and for all in the constructor:
        this._request.open('GET', 'http://openexchangerates.org/api/latest.json' + '?app_id=' + '678cd96edd4b4f3eb637bf74ef8e0815', true);
        this._request.send(null);
    }
}

Angular, service and injectability cont'd

// 'currencies-service.ts' file
import {Injectable} from '@angular/core';
…
@Injectable({providedIn: 'root'  /* 'root' injector -> Angular creates a single, shared instance... */})
export class Currencies_service {
    private _exchange_rate: … // Based on RxJS 'Subject'...
    …
    constructor() { // Get the result in an asynchronous way...
        this._request.onreadystatechange = () => { // 'window.console.warn(this._request.getAllResponseHeaders());'
            if (this._request.readyState === XMLHttpRequest.DONE) {
                if (this._request.getResponseHeader('Content-Type').includes('application/json')) {
                    const result = JSON.parse(this._request.responseText);
                    window.console.log(this._request.responseText); // Get 'timestamp'...
                    /* $ -> € and € -> $ only... */
                    if (this._from.common_symbol === '$')
                        this._exchange_rate.next(result.rates[`${this._to.iso_symbol}`]); // Based on RxJS 'Subject'...
                    else
                        this._exchange_rate.next(1 / result.rates[`${this._from.iso_symbol}`]); // Based on RxJS 'Subject'...
                }
            }
        };
    }
    …
}

Angular, service and dependency injection

// 'currencies-component.ts' file
…
// JavaScript import of service:
import {Currencies_service} from './currencies-service';
…
export class Currencies implements … {
// Note that "Dependency Injection" sets '_currencies_service' to the *SINGLETON INSTANCE* of 'Currencies_service':
    constructor(private readonly _currencies_service: Currencies_service) {
        // '_currencies_service' *has not* to be used there ('ngOnInit' instead)...
    }
    …
    public exchange_rate(from: Currency = Currencies._Currencies[Currencies.Dollar], to: Currency = Currencies._Currencies[Currencies.Euro]): void {
        if (from === to) {
            window.alert('From ' + from.iso_symbol + ' To ' + to.iso_symbol + ' ' + 1.);
            return;
        }
        this._currencies_service.exchange_rate_(from, to);
        …
    }
}

Angular, service injection scope

Access to the service may be limited in scope:

// 'currencies-service.ts' file
…
import {Currencies_module} from './currencies.module'; // This may create a circular dependency!
…
@Injectable({providedIn: Currencies_module}) // Lower scope: from 'NgModule'...
export class Currencies_service { …

Alternative:

// 'currencies.module.ts' file
…
import {Currencies_service} from './currencies-service';
…
@NgModule({ 
  …
  providers: [Currencies_service] // When you register a provider with a specific 'NgModule', the same instance of a service is available to all components in that 'NgModule'...
})
export class Currencies_module {} …

Angular, service injection scope cont'd

Access to the service may be limited a component itself.

'currencies.component.ts' file
…
@Component({
  providers: [My_stateful_service],
  selector: 'currencies',
  templateUrl: './currencies.component.html',
  styleUrls: ['./currencies.component.css']
})
export class Currencies implements OnChanges, OnDestroy, OnInit { …

Angular, service and asynchronicity

// 'currencies-service.ts' file
…
import {Subject} from 'rxjs';
…
    // To be replaced by 'BehaviorSubject' while 'AsyncSubject' is one-shot only through 'complete':
    private _exchange_rate: Subject<number> = new Subject();
    get exchange_rate(): Subject<number> { return this._exchange_rate; }
    …
    constructor() {
        …
                    if (this._from.common_symbol === '$')
                        this._exchange_rate.next(result.rates[`${this._to.iso_symbol}`]); // Based on RxJS 'Subject'...
                    else
                        this._exchange_rate.next(1 / result.rates[`${this._from.iso_symbol}`]); // Based on RxJS 'Subject'...
        …
    }

// 'currencies-component.ts' file
public exchange_rate(from: Currency = Currencies._Currencies[Currencies.Dollar], to: Currency = Currencies._Currencies[Currencies.Euro]): void {
    …
    this._currencies_service.exchange_rate_(from, to);
    this._currencies_service.exchange_rate.subscribe(amount => { // Exchange rate is got in an asynchronous way, i.e., 'next'...
        window.alert('From ' + from.iso_symbol + ' To ' + to.iso_symbol + ' ' + amount);
    });
}

Angular, form (see also here…)

Angular manages forms in two ways: reactive and template-driven forms. In short, reactive forms are for form-intensive applications: form control operates from components' inside (TypeScript code). In contrast, template-driven forms rely on specific HTML5 form validation properties like required.

// 'currencies.module.ts' file
…
import {ReactiveFormsModule} from '@angular/forms';
…
@NgModule({
    …
    imports: […, ReactiveFormsModule, …
    …
})
export class Currencies_module {}

Angular, form builder

Angular comes with a FormBuilder class in order to simplify the definition of form control (nested) pieces and related group in TypeScript code. A FormBuilder is injected as a dependency.

// 'currencies-controller.component.ts' file
…
import {FormBuilder} from '@angular/forms';
…
export class Currencies_controller {
    constructor(private readonly _form_builder: FormBuilder) { // Dependency injection
    }
    public readonly new_currency = this._form_builder.group({
        _common_name: [''],
        _common_symbol: [''],
        …
    });
    …
}

Angular, form (“New” nav.)

Angular, ngSubmit (see also here…)

The ngSubmit directive “overcomes” the original HTML action attribute, that normally post the form to a URL.

<form action="" id="my_form" name="my_form" [formGroup]="new_currency" (ngSubmit)="record_currency()">
// 'currencies-controller.component.ts' file
…
public record_currency(): void {
    // Use 'EventEmitter' to inform ascendants?
    if (this.new_currency.valid) {
      window.confirm('Form is going to be reset... status: ' + this.new_currency.status + '\nvalue: ' + JSON.stringify(this.new_currency.value));
      // Add new data in 'Currencies.Currencies'...
      this.new_currency.reset(); // Next round...
    }
}

Angular, form “monitoring” (see also here…)

Form completion may be traced on a fine-grained basis thanks to, typically, RxJS.

// 'currencies-controller.component.ts' file
…
ngOnInit() {
    this.common_name.valueChanges.subscribe(character => {
        window.console.log(JSON.stringify(character));
    });
}

Angular, form validation (see also here…)

Angular offers an enhanced support for reactive and template-driven form validation. On principle, Validators exclude the use of the HTML5 Constraint Validation API.

// 'currencies-controller.component.ts' file
…
export class Currencies_controller {
    …
    private readonly _iso_code: FormControl = new FormControl('',[Validators.required, Validators.pattern("^(?!000)(\\d{3}$)")]);
    get iso_code(): FormControl { return this._iso_code; }
    …
    public readonly new_currency = new FormGroup({common_name: this.common_name, common_symbol: this.common_symbol, description: this.description,
        iso_code: this.iso_code, iso_symbol: this.iso_symbol, substitution_date: this.substitution_date});
    …
}
<!-- 'currencies-controller.component.html' file -->
<!-- 'new_currency' as instance of 'FormGroup' is bound to the 'my_form' form with the 'formGroup' directive: -->
<form action="" id="my_form" name="my_form" [formGroup]="new_currency" (ngSubmit)="record_currency()">
    …
    <p><label>ISO code:<input formControlName="iso_code" placeholder="{{default.iso_code}}" type="text"/></label></p>
 …
</form>
<p><button form='my_form' [disabled]="new_currency.invalid" type="submit">Record currency...</button></p>

Angular, form validation cont'd

Beyond Validators as predefined validation facilities, homemade validation fonctions may be attached to FormControl and/or FormGroup (a.k.a. cross field validation) objects.

// 'currencies-controller.component.ts' file
…
export class Currencies_controller {
    static My_custom_validator: ValidatorFn = (new_currency: FormGroup): ValidationErrors | null => {
        const common_symbol = new_currency.get('common_symbol');
        const iso_symbol = new_currency.get('iso_symbol');
        return common_symbol && iso_symbol && common_symbol.value === iso_symbol.value ? {'my_custom_error': true} : null; // 'null' -> no error...
    };
    …
    public readonly new_currency = new FormGroup({common_name: this.common_name, common_symbol: this.common_symbol, description: this.description,
        iso_code: this.iso_code, iso_symbol: this.iso_symbol, substitution_date: this.substitution_date});
    …
}
<!-- 'currencies-controller.component.html' file -->
<small *ngIf="new_currency.errors?.my_custom_error && (new_currency.touched || new_currency.dirty)" class="blinking">
  Common symbol and ISO symbol must be different...
</small>

Angular, form validation: sync. versus async. (see also here… and there…)

Asynchronous validation is typically the necessity of checking form data against “remote” data, in a database for instance. It is important to note that asynchronous validation happens after synchronous validation.

export class Currencies_controller {
    …
    static My_asynchronous_custom_validator: AsyncValidatorFn = (ac: AbstractControl): Promise<ValidationErrors | null> /*| Observable<ValidationErrors | null>*/ => {
        window.confirm("This simulates some latency...");
        return Currencies.Currencies.find(currency => currency.iso_code.toString() === ac.value) ?
            Promise.resolve({my_custom_error: ac.value}) :
            Promise.resolve(null); // 'null' -> no error...
    };
    …
    private readonly _iso_code: FormControl = new FormControl('',
        {
            validators: [Validators.required, Validators.pattern("^(?!000)(\\d{3}$)")],
            asyncValidators: [Currencies_controller.My_asynchronous_custom_validator] // 'AsyncValidatorFn' objects here...
        });
    get iso_code(): FormControl {
        return this._iso_code;
    }
…
}

Angular, form validation: states (see also here…)

Form groups and controls keep real-time states between: dirty, pristine, touched, untouched, valid, and invalid.

<p>
    <label>ISO symbol:
        <input formControlName="iso_symbol" placeholder="{{default.iso_symbol}}" type="text"/>
    </label>
</p>
<small *ngIf="iso_symbol.dirty && iso_symbol.errors?.pattern">At least 3, at most 3 capital letters...</small>

More advanced stuff…

Angular, two-binding binding (see also here… and … In French)

Two-way binding is ruled by [()] within Angular template syntax. ngModel ( belonging to FormsModule) is natural helper to carry out two-way binding. A key use case is the synchronization of view pieces by means of a “shared” variable.

<input autofocus [formControl]="common_name" [(ngModel)]="two_way_binding" placeholder="{{default.common_name}}" required type="text"/>
<input formControlName="description" type="text" [(placeholder)]="two_way_binding"/>
export class Currencies_controller implements OnInit {
    …
    public readonly default = Currencies.Currencies[Currencies.Dollar];
    public readonly default_date = new Date(2020, 4, 11, 0, 0, 0, 0);
    public two_way_binding: string = this.default.common_name;

Note that [placeholder] (instead of [(placeholder)]) is enough.

Angular, two-binding binding illustration

Angular, from predefined to tailored directives (see also here… and there…)

Angular offers predefined directives as core of template syntax. Directives are backed by code to operate at run-time and differ between the following types: “component”, “attribute” (e.g., ngClass), and “structural” (e.g., ngFor). Components are first-class directives in the sense that selectors (e.g., currencies-information) augment Angular template syntax.

// 'currencies-information.component.ts' file
…
@Component({
    selector: 'currencies-information',
    template: `
        <div>
            <h2 #my_h2
                [ngClass]="{'MY_BLUE': true, 'MY_RED': information().includes(default_substitute_common_symbol)}">{{information() | uppercase}}</h2>
            <span class="material-icons">{{"?" | guess_material_icon: this.material_icon}}</span>
            <hr>
            <button (click)="back()">Back (leave from '{{this.location.path()}}')</button>
        </div>
    `,
    styles: ['div{border: 5px dashed purple;}', '.MY_BLUE{color: blue}', '.MY_RED{color: red}'] // https://angular.io/guide/component-styles ('--inline-style')
})
export class Currencies_information implements AfterViewInit, OnInit { …

Angular, using ngClass

Angular, custom directives (see also here… and there…)

Custom directives are grounded on the @Directive decorator. Custom directives must be declared in (or directly assigned to) modules in the same manner as components.

ng g d my-directive --module=my-module
@Directive({
    // Angular locates each element in the template that has an attribute named 'MY_DIRECTIVE'...
    selector: '[MY_DIRECTIVE]' // Brackets ([]) make it an attribute selector...
})
export class My_directive { …

Angular, custom directives cont'd

@Directive({
    selector: '[forbidden_ISO_code]', // Brackets ([]) make it an attribute selector...
    // Validators have to be registered so Angular is aware of them and can use them during binding:
    providers: [{provide: NG_ASYNC_VALIDATORS, useExisting: My_asynchronous_validator, multi: true}]
})
export class My_asynchronous_validator implements AsyncValidator, Iso_code_checking {
    _is_forbidden(iso_code: string): boolean {
        // Simulate some latency here...
        return Currencies.Currencies.find(currency => currency.iso_code.toString() === iso_code) ? true : false;
    }
    validate(ac: AbstractControl): Observable<ValidationErrors | null> {
// From the doc.:
// type ValidationErrors = { [key: string]: any; };
        return this._is_forbidden(ac.value) ? of({my_custom_error: ac.value}) : of(null);
    }
}
<p>
    <label>ISO code:
        <input forbidden_ISO_code formControlName="iso_code" placeholder="{{default.iso_code}}" type="text"/>
    </label>
</p>

Angular, pipes (here… and there…)

Angular comes with built-in pipes (e.g., date, decimal, percent). Roughly speaking, a pipe takes in a value or values and then returns a value. The @Pipe decorator allows the creation of custom pipes.

Built-in pipe

{{information() | uppercase}}

Homemade pipe

ng g pipe ../currencies/guess-material-icon -f --skipTests
{{"?" | guess_material_icon: this.material_icon}}

Angular, custom pipe - implementation (see also here…)

// 'guess-material-icon.pipe.ts' file
import {Pipe, PipeTransform} from '@angular/core';

// A pure pipe is only called when Angular detects a change in the value or the parameters passed to a pipe.
// An impure pipe is called for every change detection cycle no matter whether the value or parameter(s) change(s).
@Pipe({name: 'guess_material_icon', pure: false})
export class Guess_material_icon implements PipeTransform {
// Bad architecture (values in array are shared with 'currencies-menu' component):
    private static readonly _Currency_material_icons = new Array('attach_money', 'euro_symbol'); // '$', '€'

    transform(a_priori_value: string, value: string): string {
        if (a_priori_value !== "?") return a_priori_value;
        const guessed_material_icon = Guess_material_icon._Currency_material_icons.find(material_icon => material_icon === value);
        return guessed_material_icon ? guessed_material_icon : 'error';
    }
}

Angular, custom pipe - utilization

// 'currencies-information.component.ts' file
…
@Component({
    selector: 'currencies-information',
    template: `
        <div>
            <h2 #my_h2 [ngClass]="{'blue': true, 'red': information().includes(default_substitute_common_symbol)}">{{information() | uppercase}}</h2>
            <span class="material-icons">{{"?" | guess_material_icon: this.material_icon}}</span>
            <hr><button (click)="back()">Back (leave from '{{this.location.path()}}')</button>
        </div>
    `,
    styles: ['div{border: 5px dashed purple;}', '.blue{color: blue}', '.red{color: red}'] // https://angular.io/guide/component-styles ('--inline-style')
})
export class Currencies_information implements AfterViewInit, OnInit {
    …
    private _material_icon: string;
    get material_icon(): string { return this._material_icon; }
    set material_icon(material_icon: string) { this._material_icon = material_icon; }
    ngOnInit() {
        this._navigation.paramMap.subscribe((parameters: ParamMap) => { // Find 'material_icon' from 'iso_code':
            const currency = Currencies.Currencies.find(currency => currency.iso_code.toString() === parameters.get("iso_code") /* '840' for Dollar, etc. */);
            this.material_icon = currency && currency.hasOwnProperty('material_icon') ? currency.material_icon : "?";
        });
        …
    }
}

Angular, sibling component interaction based on injected service

import {EventEmitter, Injectable} from '@angular/core';
@Injectable({ providedIn: 'root' })
export class Change_route_service {
    private readonly _peer_to_peer: EventEmitter<string> = new EventEmitter();
    public receive_path(handler: (path: string) => void): void {
        this._peer_to_peer.subscribe(handler);
    }
    public send_path(path: string): void {
        this._peer_to_peer.emit(path);
    }
}

Angular, sibling component interaction based on injected service cont'd

// 'currencies-controller.component.ts' file
…
import {Change_route_service} from "./change-route-service";
…
export class Currencies_controller implements OnInit {
    …
    constructor(private readonly _change_route_service: Change_route_service) {} // Dependency injection
    public record_currency(): void {
        …        
        this._change_route_service.send_path('Currencies'); // Next round...
    }       
}
// 'currencies-menu.component.ts' file
…
import {Router, Routes} from '@angular/router';
import {Change_route_service} from "./change-route-service";
…
export class Currencies_menu implements OnInit {
    …
    constructor(private readonly _change_route_service: Change_route_service, private readonly _router: Router) {}
    ngOnInit() {
        this._change_route_service.receive_path((path: string) => {
            this._router.navigate([path]);
        });
    }
}

Angular, test (see also here… In French)

Unit testing is based on contract programming (assertions) that relies on the expect keyword.

const injector = Injector.create({
  providers:
      [{provide: NeedsService, deps: [UsefulService]}, {provide: UsefulService, deps: []}]
});
expect(injector.get(NeedsService).service instanceof UsefulService).toBe(true);

© Franck Barbier