image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Les templates sont des documents HTML spécifiant comment le rendu d'un composant doit être réalisé. Le contenu d'un template peut être directement explicité dans l'entrée template de l'annotation @Component d'un composant ou alors nous pouvons indiquer un lien vers un fichier externe dans l'entrée templateUrl.

Toute balise HTML peut être utilisée dans un template Angular à l'exception notable de <script> : la génération de code JavaScript est de la reponsabilité du framework Angular, ainsi nous n'avons aucun intérêt à écrire du code JavaScript à la main dans un template. De plus si ce code dépendait d'une entrée de l'utilisateur, il y aurait un risque potentiel d'injection de code malveillant.

Les balises HTML "de haut niveau" (<html>, <body>...) n'ont pas à être utilisées dans un template : elles sont employées une seule fois dans le fichier src/index.html racine. La philosophie d'Angular est de créer des applications web utilisant une unique page HTML dans laquelle l'arbre DOM est dynamiquement modifié en fonction des intéractions avec l'utilisateur ou des services web.

Nous supposons que le lecteur de cette page connaît déjà le langage HTML, ainsi nous nous intéresserons plus particulièrement aux directives de template introduites par Angular.

Notons qu'un template Angular doit être traduit en code JavaScript pour pouvoir être utilisé. La compilation d'un template peut se faire de deux manières :

Pour activer la compilation AoT des templates pour le serveur de développement on pourra utiliser la commande ng serve --aot

Un aperçu des directives de template Angular

Directives d'expressions

Nous présentons ici un résumé des différentes manières d'incorporer une expression dans un template Angular :

Directives structurelles

Les directives structurelles permettent de modifier l'arbre DOM en ajoutant ou supprimant des éléments. Elles permettent notamment d'introduire des structures de contrôle au sein des templates pour exécuter conditionnellement certaines parties ou alors pour réaliser des boucles permettant d'afficher plusieurs éléments.

Les directives structurelles sont des attributs de balise toujours préfixés par une asterisque.

Directives conditionnelles

La directive *ngIf permet d'afficher un composant enfant uniquement si une expression du composant courant est évaluée vraie. Dans l'exemple qui suit, la balise <div> ne sera affichée que si le champ this.isConnected du composant est vrai :

<div *ngIf="isConnected">You are connected and your login is {{ userName }}</div>

Les directives *ngSwitchCase, *ngSwitchDefault offrent la possibilité de proposer plusieurs alternatives en fonction de la valeur d'une expression :

<ng-container [ngSwitch]="order.status">
	<div *ngSwitchCase="'pending'">
		Vous n'avez pas encore validé votre commande
	</div>
	<div *ngSwitchCase="'received'">
		Votre commande {{ order.id }} est reçue et en cours de traitement
	</div>
	<div *ngSwitchCase="'payed'">
		Votre paiement a été validé, nous préparons votre commande
	</div>
	<div *ngSwitchCase="'shipped'">
		<p>Votre commande a été expédiée avec le numéro de suivi {{ order.parcel.trackingNumber }}, voici son suivi : </p>
		<parcel-tracking [parcel]="order.parcel"></parcel-tracking>
	</div>
	<div *ngSwitchCase="'delivered'">
		Votre commande a été livrée avec succès !
	</div>
	<div *ngSwitchDefault>
		Désolé, un problème est survenu dans la gestion de votre commande, veuillez nous contacter
	</div>
</ng-container>

Dans les exemples présentés, les directives ont été appliquées sur des balises <div> ; il est possible des les appliquer sur n'importe quelle balise ou composant Angular.

☞ La balise ng-container ne rajoute pas de nœud dans l'arbre DOM.

Directive de boucle

La directive *ngFor permet d'afficher plusieurs fois un composant pour différents éléments d'une variable itérable. Par exemple, une page peut afficher le panier d'achat d'un client d'une boutique en ligne :

<app-basket-item *ngFor="let item of items" [item]="item"></app-basket-item>

Il est possible aussi de récupérer l'indice de chaque item affiché ainsi :

<div *ngFor="let item of items; let i = index">
	Voici l'item {{ i }} du panier :
	<app-basket-item [item]="item"></app-basket-item>
</div>

Si la liste est modifiée (par exemple par un autre composant sur le template), la région de l'arbre DOM affichant le panier devra être complétement recalculée. Il est possible de limiter le recalcul en associant à chaque item un identifiant unique. Si Angular retrouve des items de même identifiants après modification, il réutilisera les sous-arbres DOM déjà générés.

<app-basket-item *ngFor="let item of items; trackBy: trackByItems"></app-basket-item>

Il est nécessaire d'implanter une méthode trackByItems dans le composant afin de retourner un identifiant unique pour un item, ici la référence produit de l'item :

trackByItems(index: number, item: Item): string { 
	return item.reference; 
}

Expressions

Il est préconisé de respecter les principes essentiels suivants :

Nommage de composant

Il est possible de donner un nom à un composant d'un template afin d'y faire référence. Voici un exemple où nous disposons d'un champ de formulaire input et souhaitons afficher sa valeur dans le template :

<input #dataInput>
Valeur du champ : {{ dataInput.value }}

dataInput peut aussi être accessible à l'intérieur du code du composant en ajoutant le champ suivant :

@Component( { ... })
export class CustomComponent {
	...
	@ViewChild('dataInput') dataInputRef: ElementRef;
	
	doSomeAction() {
		...
		let valueInDataInput = dataInputRef.nativeElement.value;
		...
	}
}

Pipes

Les expressions peuvent être transformées par des pipes. Il est possible d'utiliser des pipes fournis par l'API (lowercase, uppercase, currency, json, slice, percent, date...). Par exemple si nous souhaitons formater un pourcentage, nous pouvons employer le pipe percent :

/**
 * Affiche le nombre a en pourcentage en utilisant au moins 4 chiffres avant la virgule 
 * et entre 3 et 5 chiffres après la virgule avec la locale française (utilisation de virgule comme séparateur décimal)
 * Avec l'exemple a = 1.458, l'affichage sera 0145,800 %
 */
@Component({
      selector: 'percent-test',
      template: `<div> {{ a | percent:'4.3-5':'fr' }} </div>`
})
export class PercentPipeComponent { a: number = 1.458; }

Il est possible aussi d'écrire ses propres pipes en implantant l'interface PipeTransform. Ici nous écrivons un pipe renversant une chaîne :

import { Pipe, PipeTransform } from '@angular/core';

/** Pipe transforming a string by reversing the order of characters */
@Pipe({name: 'reverse'})
export class ReversePipe implements PipeTransform {
  transform(value: string): string {
    return s.split("").reverse().join("");
  }
}

Expressions immuables et changeantes

Les expressions introduites dans {{ }} et [ ] peuvent être immuables ou peuvent avoir une valeur changeante. Si la valeur change, Angular doit être en mesure de le détecter.

Angular déclenche le détecteur de changement sur l'arbre des composants lorsqu'un événement a lieu (clic sur un composant par exemple, réception du résultat d'une requête HTTP, exécution d'une fonction suite à un setTimeout ou setInterval...). Chacune des expressions du template de chaque composant est réévaluée : si sa valeur est modifiée, l'arbre DOM représentant le composant sera reconstruit. Afin de détecter une modification d'expression, seul un test d'égalité superficiel est réalisé. Cela signifie qu'un tableau modifié ou un objet dont une propriété a été modifiée ne sera pas considéré comme différent et ne déclenchera pas une reconstruction de la vue.

Les expressions pouvant être réévaluées souvent, il est important que leur calcul utilise peu de ressources. Il est préférable de mentionner des champs du composant comme expressions plutôt que des méthodes (possiblement coûteuses).

Voici un exemple de composant affichant l'heure courante à travers un champ ou une méthode. Par ailleurs un tableau est mélangé (en place) toutes les secondes. On constatera que si l'on conserve le même tableau (modifié), l'affichage n'est pas mis à jour. Les méthodes ngOnInit et ngOnDestroy sont respectivement appelées à l'initialisation de la vue et lors de sa destruction : nous désactivons l'exécution périodique de la fonction avec clearInterval lors de la destruction.

import { Component, OnInit, OnDestroy } from '@angular/core';
import { shuffle } from '../utils/arrays';

@Component({
  selector: 'app-mutability',
  template: `<div>
	<p>Calling method getCurrentDate(): {{ getCurrentDate() | date:'fullTime' }}</p>
	<p>Displaying now field: {{ now | date:'fullTime' }}</p>
	<p>Content of the array (shuffled each second): {{ shuffledArray }}</p>
	<p>Content of a copy of the array: {{ copiedShuffledArray }}</p> 		
  </div>`,
  styleUrls: ['./mutability.component.css']
})
export class MutabilityComponent implements OnInit, OnDestroy {
  now: Date;
  shuffledArray = [1, 2, 3, 4, 5, 6];
  copiedShuffledArray: number[];
  intervalHandle;

  constructor() {
	this.now = this.getCurrentDate();
  }

  getCurrentDate(): Date {
	return new Date();
  }

  ngOnInit() {
	this.intervalHandle = setInterval(() => {
		this.now = this.getCurrentDate();
		shuffle(this.shuffledArray);
		this.copiedShuffledArray = this.shuffledArray.slice(0, this.shuffledArray.length);
	}, 1000);
  }

  ngOnDestroy() {
	if (this.intervalHandle)
		clearInterval(this.intervalHandle);
  }
}

L'exécution de l'algorithme de détection de changement à chaque événement peut être coûteux. Si l'état d'un composant ne dépend que de la valeur de ses propriétés d'entrée (annotées avec @Input), alors il est recommandé d'utiliser la stratégie de détection de changement onPush. La détection de changement ne sera réalisée que si les propriétés d'entrée sont modifiées. Voici un exemple avec un composant affichant le nom et le prénom d'une personne :

import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
import { Person } from "../model/person"

@Component({
  selector: 'app-person',
  template: `<div>Component ID: {{ id }}, FirstName: {{ person.firstName }}, LastName: {{ person.lastName }}</div>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class PersonComponent {
  @Input() person: Person;

  /** Giving an unique incrementing id for each component */
  static counter = 0;
  id = PersonComponent.counter++;

  constructor() { }
}

Nous pouvons alors embarquer le composant Person dans un composant parent :

import { Component, Input, ChangeDetectionStrategy, OnInit, OnDestroy } from '@angular/core';
import { Person } from "../model/person"

@Component({
  selector: 'app-couple',
  template: `<div>Here is a couple: 
	<app-person *ngFor="let p of [person1, person2]" [person]="p"></app-person>
	</div>`
})
export class CoupleComponent implements OnInit, OnDestroy {
  person1 = new Person("Jane", "Doe");
  person2 = new Person("John", "Doe");
  
  timeoutHandle;
  
  constructor() { }
  
  ngOnInit() {
	this.timeoutHandle = setTimeout(() => { this.switchPeople(); }, 10000);
  }
  
  ngOnDestroy() {
    clearTimeout(this.timeoutHandle);
  }
  
  switchPeople() {
	let tmp = this.person1;
	this.person1 = this.person2;
	this.person2 = tmp;
  }
}

S'il survient un événement au cours duquel nous appelons la méthode switchPeople, les attributs d'entrée des composants Person sont réévalués. Angular détecte que les références d'objet restent les mêmes mais dans un ordre différent dans le tableau [person1, person2] : les composants sont échangés dans l'arbre DOM (ce que nous vérifions avec les ids affichés). Il n'y a pas de tentative de reconstruction de l'arbre DOM de chaque composant car la référence d'objet du paramètre d'entrée person ne change pas et que nous utilisons la stratégie de détection onPush.

Modifions maintenant la méthode switchPeople2 :

switchPeople2() {
	let tmp = [this.person1.firstName, this.person1.lastName];
	[this.person1.firstName, this.person1.lastName] = [this.person2.firstName, this.person2.lastName];
	[this.person2.firstName, this.person2.lastName] = tmp;
}

Les références d'objet ne sont pas modifiées : c'est le contenu des champs qui est échangé. Aucun changement sur les paramètres d'entrée n'est détecté par Angular lorsque survient l'événement aucours duquel la méthode switchPeople2 est appelée. La stratégie onPush étant utilisé sur le composant Person, aucune détection de changement n'est mené sur ces composants. Avec la stratégie par défaut, l'affichage aurait été rafraíchi.

Il en résulte que la stratégie de détection de changement onPush doit être réservée aux composants :

  1. dont l'état ne dépend que des paramètres @Input
  2. et dont ces paramètres sont immuables (le contenu des objets ne doit pas être modifiable)

Nullabilité des types

Une variable typée avec TypeScript ne peut prendre la valeur null ou undefined sauf si cela est explicitement déclaré. Ainsi une variable person: Person ne peut pas être nulle contrairement à person: Person|null. Ainsi la fonction suivante devrait poser un problème car person pourrait être nul :

function getPersonInitials(person: Person|null) {
	return person.firstName.charAt(0) + person.lastName.charAt(0);
}

Par défaut TypeScript ne réalise pas de vérification stricte de nullité et autorise la compilation de cette méthode. Il est possible d'activer la vérification stricte de nullité en modifiant le fichier tsconfig.json à la racine du projet :

{
  "compilerOptions": {
	...
    "strictNullChecks": true
}

Si cette vérification est activée globalement et qu'une variable peut être nulle, il est possible de désactiver la vérification dans une expression de template en la suffixant par un ! :

Name of the person: {{ person!.name }}

Si person a une valeur nulle à l'exécution cela provoquerait une exception ; il est donc plus sûr de tester la nullité ainsi que la valeur undefined :

<div *ngIf="person !== null && person !== undefined">
	Name of the person: {{ person.name }}
</div>

Angular propose aussi un opérateur d'accès ?. sécurisé qui permet d'accéder à un membre d'un objet possiblement nul. Si l'objet est nul, le rendu de l'expression sera une chaîne vide :

Name of the person: {{ person?.name }}

Gestion d'événements

Événements standards

L'exécution d'une instruction peut être demandée dans un template suite à la survenue d'un événement. Par exemple si l'on souhaite appeler la méthode doAction() (implantée dans le composant) suite au clic sur un bouton, on écrira :

<button (click)="doAction()">Do the action</button>

A priori tous les événements standards JavaScript peuvent être supportés. Voici l'exemple d'un composant affichant une zone div rouge exploitant l'événement mouseover pour afficher les coordonnées actuelles de la souris dans la zone :

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

@Component({
  selector: 'app-divmove',
  template: `<div>
	<div class="divzone" (mousemove)="updateCoordinates($event)"></div>
	<ul *ngIf="x !== null && y !== null">
		<li>x = {{ x }}</li>
		<li>y = {{ y }}</li> 
	</ul>
	<div *ngIf="x === null">
		Not already entered in the red div zone!
	</div>
  `,
  styles: [`.divzone { width: 300px; height: 300px; background-color: red }`]
})
export class DivmoveComponent {

  x: number|null = null;
  y: number|null = null;

  updateCoordinates(event) {
	[this.x, this.y] = [event.offsetX, event.offsetY];
  }
}

Événements personnalisés

Un composant peut définir un événement personnalisé qu'il peut émettre dans certaines circonstances. Pour cela, nous définissons un champ EventEmitter<T> dans le composant que nous annotons avec @Output() :

@Component({ ... })
export class CustomComponent {
	@Output myEvent = new EventEmitter<number>(); // événement émis sous la forme d'un nombre
	
	someMethod() {
		...
		// Nous émettons l'événement
		this.myEvent.emit(42);
	}
}

L'événement peut être capturé dans le template d'un composant parent pour faire une action :

<app-custom (myEvent)="doSomeAction()"></app-custom>

À titre d'application, implantons une machine à sous simplifiée avec deux rouleaux de symboles de 1 à 7. Nous empochons 7 fois notre mise si nous obtenons 2 symboles identiques, sinon nous perdons notre mise.

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-slot-machine',
  template: `<div *ngIf="draw !== null">Slot machine draw: {{ draw[0] }} - {{ draw[1] }}</div>
			 <div *ngIf="draw === null">Slot machine not used yet</div>`
})
export class SlotMachineComponent  {
  readonly minSymbol = 1;
  readonly maxSymbol = 7;
  readonly betMultiplier = 7; // the bet is multiplied by this value when we win
  
  // money bet in the slot machine
  @Input() bet: number;
  
  draw: [number, number] | null = null;
  
  @Output() gain = new EventEmitter<number>();
  
  randomInt() { return Math.floor(Math.random() * (this.maxSymbol - this.minSymbol) + 1) + this.minSymbol; }
  
  doNewDraw() {
	this.draw = [this.randomInt(), this.randomInt()];
	if (this.draw[0] === this.draw[1])
		this.gain.emit( this.bet * this.betMultiplier ); // we win and multiply the bet
	else
		this.gain.emit ( - this.bet ); // we lose the bet
  }
}

Nous pouvons incorporer notre composant machine à sous dans un composant casino gérant le solde d'un compte de joueur de casino. Ce composant enregistre une méthode à appeler lorsque l'événement gain survient sur la machine à sous afin de mettre à jour le solde du compte du joueur :

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

@Component({
  selector: 'app-casino',
  template: `<div>
	<p>Welcome in the casino!</p>
	<app-slot-machine #machine [bet]="bet" (gain)="updateBalance($event)"></app-slot-machine>
	<div *ngIf="lastGain > 0">Congratulations, you won {{ lastGain }}</div>
	<div *ngIf="lastGain < 0">Sorry, you lost {{ -lastGain }}</div>
	Place your bet: <input [value]="bet" (input)="setBet($event.target.value)"> <br>
	<button *ngIf="balance >= bet" (click)="machine.doNewDraw()">Draw for a bet of {{ bet }}</button> <br>
	Balance of your account: {{ balance }}
  </div>
   `
})
export class CasinoComponent implements OnInit {
  
  @Input() initialBalance: number = 100;
  balance: number;
  bet = 0;
  lastGain = 0;

  ngOnInit() {
	this.balance = Math.floor(this.initialBalance);
  }
  
  setBet(bet) {
	bet = Math.floor(bet);
	if (! isNaN(bet)) this.bet = bet;
  }
  
  updateBalance(amount) {
    this.lastGain = amount;
	this.balance += amount;
  }
}

Crochets (hooks) du cycle de vie

Un composant peut implanter des méthodes qui seront appelées lors d'événements de son cycle de vie :

Liaisons bidirectionnelles

Une liaison bidirectionnelle exprimée avec la syntaxe "banane dans la boîte" [( )] permet de fournir une valeur pour l'attribut d'un composant mais également d'étre informé des modifications réalisées par le composant sur cet attribut.

Formulaire basé sur un template

L'établissement de liaisons bidirectionnelles est utile pour les champs de formulaire. Nous proposons ici un exemple implantant un convertisseur de devises muni d'une zone de texte afin de rentrer une valeur numérique ainsi qu'un élément select afin de choisir la devise de la valeur. Une liste de conversions est affichée pour la valeur entrée.

Tout d'abord notons que l'utilisation de formulaires nécessite l'import de FormModule dans app.module.ts :

import { FormsModule }   from '@angular/forms';
...
@NgModule({
  ...
  imports: [
    BrowserModule,
    FormsModule
  ],
  ...
})

Nous écrivons ensuite le composant CurrencyConverter :

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

class Currency {
	// value is the number of units we can buy with one euro
	constructor(public name: string, public value: number) {}
}

class MoneyAmount {
	constructor (public quantity: number, public currency: Currency) {}
	
	convert(otherCurrency: Currency): MoneyAmount {
		let newQuantity = this.quantity * this.currency.value / otherCurrency.value;
		return new MoneyAmount(newQuantity, otherCurrency);
	}
	
	toString() {
		return `${this.quantity.toFixed(2)} ${this.currency.name}`;
	}
}

const CURRENCIES = [
	new Currency("EUR", 1),
	new Currency("FRF", 1/6.55957),
	new Currency("USD", 1/1.14),
	new Currency("GBP", 1/0.9),
	new Currency("BTC", 1/0.0003) ];

@Component({
  selector: 'app-currency-converter',
  template: `<div>
	<form #currencyForm="ngForm">
		<input type="number" [(ngModel)]="moneyAmount.quantity" name="quantity" id="quantity">
		<select name="currency" id="currency" [(ngModel)]="moneyAmount.currency">
			<option *ngFor="let currency of currencies" [ngValue]="currency">{{ currency.name }}</option>
		</select>
	</form>
	<ul>
		<li *ngFor="let currency of currencies">{{ moneyAmount.convert(currency) }}</li>
	</ul>
	<button (click)="multiply(2)">Multiply by 2</button>
  </div>`,
  styleUrls: ['./currency-converter.component.css']
})
export class CurrencyConverterComponent {
	moneyAmount: MoneyAmount = new MoneyAmount(1, CURRENCIES[0]);
    currencies = CURRENCIES;
    
    multiply(factor) {
		this.moneyAmount.quantity *= 2;
	}
}

La liaison bidrectionnelle est mise en œuvre avec l'attribut [(ngModel}]=".." qui permet d'indiquer à quelle valeur du modèle est liée la valeur du champ de formulaire. La liaison fonctionne dans les deux sens : une modification de modèle entraînera une modification de la valeur affichée dans le champ ([]) et une modification de la valeur du champ entraîne un événement mettant à jour la valeur du modèle (()).

Le module de formulaires d'Angular offre des fonctionnalités supplémentaires afin de valider les valeurs des champs et de vérifier ainsi que l'utilisateur a entré des données valides.

Ce chapitre de la documentation Angular traite plus en détails du fonctionnement des formulaires.

Création de liaisons bidirectionnelles

Nous pouvons créer des liaisons bidirectionnelles sur nos composants. Le principe consiste à créer deux propriétés dans la classe du composant l'une nommée x annotée avec @Input() pour recevoir une valeur et l'autre nommée xChange avec @Output() pour un émetteur d'événement prévenant d'un changement de valeur :

@Input()  x: type;
@Output() xChange = new EventEmitter<type>();

Nous appliquons ce principe afin de réaliser un composant permettant d'entrer une suite de chiffres à l'aide d'un pavé numérique (composant utilisé sur certains sites bancaires pour limiter les risques liés à l'espionnage des touches de clavier) :

import { Component, EventEmitter, Input, Output } from '@angular/core';

import { shuffle } from '../utils/arrays';

@Component({
  selector: 'app-keypad',
  template: `<div>
	<table>
		<tr *ngFor="let i of [0, 3, 6]">
			<td *ngFor="let j of [i, i+1, i+2]">
				<button (click)="appendDigit(this.digits[j])">{{ this.digits[j] }}</button>
			</td>
		</tr>
	</table>
	{{ value }}
  </div>
  `
})
export class KeypadComponent {
  readonly digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
  
  @Input() value: string = "";
  @Output() valueChange = new EventEmitter<string>();
  
  constructor() { 
	shuffle(this.digits);
  }

  appendDigit(j) {
	this.value += "" + j;
	this.valueChange.emit(this.value);
  }
}

Embarquons le composant Keypad dans un composant parent KeypadWrapper :

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

@Component({
  selector: 'app-keypad-wrapper',
  template: `<app-keypad [(value)]="keypadValue"></app-keypad>
	<button (click)="keypadValue = ''">Reset</button>`,
  styleUrls: ['./keypad-wrapper.component.css']
})
export class KeypadWrapperComponent implements OnInit {

  keypadValue: string = "";

  constructor() { }

  ngOnInit() {
  }

}

Formulaires réactifs

Angular propose deux types de formulaires :

  1. Les formulaires basés sur des templates. Ils exploitent les liaisons bidirectionnelles déclarées dans le template.
  2. Les formulaires réactifs. Ils sont plus flexibles et modifiables dynamiquement. La structure du formulaire réactif est déclaré dans le composant ; le template permet d'organiser graphiquement l'affichage des champs

Nous nous intéressons ici aux formulaires réactifs. La première étape pour leur emploi consiste à déclarer ReactiveFormsModule dans app.module.ts :

import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  imports: [
    // other imports ...
    ReactiveFormsModule
  ],
})
export class AppModule { }

Un composant nécessitant un formulaire peut utiliser un FormBuilder qui permet de déclarer la structure du formulaire composée de groupes, de tableaux (nœuds internes) ainsi que de contrôles (feuilles). On peut créer un composant avec un formulaire ainsi construit :

import { Component, OnInit } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { Validators } from '@angular/forms';

import { DirectoryService } from '../directory.service';

@Component({
  selector: 'app-directory',
  templateUrl: './directory.component.html',
  styleUrls: ['./directory.component.css'],
  providers: [DirectoryService]
})
export class DirectoryComponent implements OnInit {

  form = this.fb.array([]);
  lastSave: Date|null = null;
  
  createDirectoryEntryForm() {
	return this.fb.group({
		name: ['', Validators.required],
		coordinates: this.fb.group({
			phone: ['', Validators.pattern("[0-9 ]+")],
			email: ['', Validators.email]
		})
	});
  }
  
  addDirectoryEntry(value) {
	let f = this.createDirectoryEntryForm();
	f.setValue(value);
	this.form.push(f);
  }
  
  removeDirectoryEntry(position) {
	this.form.removeAt(position);
  }
  
  save() {
	this.directoryService.saveEntries(this.form.value).subscribe( result => { if (result) this.lastSave = new Date() });
  }
  
  constructor(private directoryService: DirectoryService, private fb: FormBuilder) { }
  
  ngOnInit() {
	this.directoryService.loadEntries().subscribe( value => this.addDirectoryEntry(value) );
  }
  
  ngOnDestroy() {
  }
	
}

Initialement la formulaire principal directoryContent est un tableau vide. Nous ajouterons ensuite de nouvelles entrées avec la méthode addDirectoryEntryForm(). L'objectif est de réaliser une application permettant de gérer un carnet d'adresses (avec téléphone et adresse email pour chaque personne).

Pour chaque champ de formulaire utilisé, nous pouvons ajouter un validateur (Validators.required pour name par exemple) vérifiant la validité de la valeur entrée. Il est possible de créer ses propres validateurs ou d'utiliser des validateurs existants. Il est possible de créer des validateurs synchrones (d'exécution rapide pour calculer immédiatement si la valeur est valide) ou asynchrones (qui peuvent par exemple nécessiter d'interroger un serveur web). Ainsi le validateur pattern utilisée pour l'adresse email est synchrone (validant avec une expression régulière). Nous aurions pu aussi écrire notre propre validateur asynchrone interrogeant un service web vérifiant si le nom de domaine de l'adresse email est bien valide (avec une requête DNS).

On peut ajouter globalement à un groupe de contrôles d'un formulaire un validateur vérifiant la cohérence de plusieurs champs entre-eux.

Pour afficher les champs avec leur contenu dans le template, nous accédons à form déclaré dans le composant :

<div>
	<form [formGroup]="form" (ngSubmit)="save()">
		<div *ngFor="let dirEntry of form.controls; let i=index" style="border: dotted">
				<div [formGroupName]="i">
					<label>Name: <input type="text" formControlName="name"></label>  <a (click)="removeDirectoryEntry(i)" style="background-color: red">Remove</a>
					<div formGroupName="coordinates">
						<label>Phone: <input type="text" formControlName="phone"></label>
						<label>Email: <input type="text" formControlName="email"></label>
					</div>
				</div>
		</div>
		<button (click)="addDirectoryEntry({name: '', coordinates:{phone: '', email: ''} }); false">Add entry</button>
		
		<button type="submit" [disabled]="!form.valid">Save</button>
		
		<p *ngIf="lastSave">Last save: {{ lastSave| date:"full" }}</p>
	</form>
</div>

On peut ensuite consulter form déclaré dans DirectoryComponent pour avoir des informations sur l'état du formulaire :

Internationalisation (i18n)

Internationaliser une application consiste à la rendre disponible dans différentes langues. Pour cela on réalise un template dans une langue par défaut et on marque les chaînes traduisibles en ajoutant un attribut i18n="@@identifierOfTheString". Chaque identifiant i18n doit être unique. Il est possible aussi de traduire des attributs de tag en ajoutant un attribut i18n-nameOfTheAttribute="@@identifierOfTheString". Voici un exemple simple :

<h1 i18n="@@pictureDisplayer">Picture displayer</h1>
<img src="picture.jpg" alt="Displayed picture" i18n-alt="@@pictureCaption">

Il est conseillé aussi d'utiliser les pipes suivants qui facilitent l'affichage de données localisées :

On peut générer un fichier messages.xlf réunissant l'ensemble des chaînes i18n avec la commande suivante :

ng xi18n --output-path src/locale

Par défaut Angular utilise le format XML xliff pour l'internationalisation. Il existe des éditeurs spécialisés pour ce type de fichiers comme Poedit.

Si nous souhaitons créer une traduction française, nous copions le fichier messages.xlf en un fichier messages.fr.xlf que nous pouvons éditer pour fournir les traductions des chaînes. Pour tester la nouvelle locale, on lance la commande :

ng serve --aot --configuration=fr

Malheureusement à l'heure actuelle Angular ne gère pas la détection automatique de langue préférée du navigateur afin d'adapter dynamiquement la locale. Pour pallier ce manque, une approche envisageable consiste à compiler plusieurs versions de l'application pour chacune des langues et à configurer le serveur web utilisé pour qu'il redirige l'internaute vers la version la plus adaptée (exemple pour Nginx).