*Certains items peuvent glisser sur jour précédent/suivant selon attente et/ou niveau de l'audience.
ng
(standing for “Angular”) command,
which itself relies on the npx
npm package executor.
npm install @angular/core
npm install rxjs@^6.5.3
npm install zone.js@~0.10.2
-g
parameter) as follows:
npm install -g @angular/cli
. Key options (
new
, generate
, etc.) are then knowable as follows:
ng help
.
ng new FranckBarbier --prefix=FB --routing
.
src
contains the
« grey matter » of the app. to be built.
tsconfig.json
file.
.css
and
.html
files. The
--inline-style
and
--inline-template
arguments force the
“embedded” option. This may occur component by component or at the application level:
ng new FranckBarbier -s -t -S
spec.ts
suffix.
The --skipTests
argument (a.k.a. -S
)
disables unit testing. This may also occur piece by piece:
ng g c ../currencies/currencies-information -f --skipTests
Unit testing is based on Karma
while end-to-end testing (user interaction) relies on
Protractor (e2e
directory).
To that extent, appropriate files are created in the project's Angular workspace
like, for instance, karma.conf.js
.
angular.json
file.
This files records the app./project discriminating features. For example, instead of
app
, naming prefix for Angular components might be FB
:
"prefix": "FB",
angular.json
is subject to the use of
the ng config
command. Variations (reading or writing data):
ng config version
ng config version 2
ng config projects.FranckBarbier.architect.build.options.main
ng add sweetalert2
.
Library access is then ruled by JavaScript 6/TypeScript import clause:
import Swal from "sweetalert2";
ng serve
command (run first cd FranckBarbier
) launches a “live” Web server to get the app. functioning in a browser
(browser-based execution is the common option). Variations:
ng serve --host=localhost --port=1963
ng build
command is concerned with compilation and packaging. Variation (creation of production version):
ng build --aot --prod
Variation:
ng build --verbose
ng test
launches unit testing. Variation:
ng test --progress=true
ng e2e
launches unit testing. Variation:
ng e2e --host=localhost --port=1963
ng generate module currencies
.
--routing
is used.
currencies
or libraries offered through predefined modules.
ng g component ../currencies/currencies --flat
).
It is basically embodied by a TypeScript class.
// '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 {}
// '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 {}
// '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");}
}
// 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; }
}
currencies-wrapper
renderingAngular 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>
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>
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>
ngStyle
(examples
here…)
for dynamically changing CSS style values.
<a on-mouseover="send(currency)" [type]="currency.common_name"
[ngStyle]="{'color': checked(currency) ? 'green' : 'red'}">
{{currency.common_name}}
</a>
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()'...
}
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>
…
<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;
}
…
}
<!-- '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);
}
}
// 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; }
}
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; }
}
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 offers support for managing navigation that leads to view changes in a Single Page Application (SPA) logic (further detail here…).
// '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 {}
<!-- '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> -->
<!-- '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>
// '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!) */
];
…
}
// '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.
});
}
}
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]);
});
}
}
// '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...
}
}
…
}
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);
}
}
// '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'...
}
}
};
}
…
}
// '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);
…
}
}
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 {} …
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 { …
// '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 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 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: [''],
…
});
…
}
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...
}
}
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 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>
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>
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;
}
…
}
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>
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 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 { …
ngClass
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 { …
@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 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}}
// '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';
}
}
// '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 : "?";
});
…
}
}
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);
}
}
// '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]);
});
}
}
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);