Pour des applications web modernes, vous aurez besoin de formulaires complexes et dynamiques. Angular vous propose les formulaires réactifs.
Là où les formulaires template sont définis par le contenu HTML du template, les formulaires réactifs sont d'abord générés en TypeScript – on vient ensuite relier les différents input
du template à l'objet du formulaire.
Les formulaires réactifs offrent beaucoup plus de possibilités aux développeurs :
comme leur nom l'indique, les formulaires réactifs mettent à disposition des Observables pour réagir en temps réel aux valeurs entrées par l'utilisateur ;
les formulaires réactifs permettent une validation beaucoup plus approfondie ;
pour générer des formulaires totalement dynamiques – c'est-à-dire des formulaires dont vous ne connaissez pas la structure en amont – les formulaires réactifs sont la seule solution.
Dans l'application Snapface, vous allez créer un formulaire réactif qui permet enfin aux utilisateurs d'enregistrer leurs propres FaceSnaps !
Importez ReactiveFormsModule
Comme pour les formulaires template, il faut ajouter un import à AppModule. Pour les formulaires réactifs, il faut importer ReactiveFormsModule
:
// ...
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule
],
//...
Préparez le component
Vous allez créer un nouveau component pour le formulaire, auquel vos utilisateurs accéderont par une nouvelle route. Générez un nouveau component appelé NewFaceSnapComponent avec le CLI :
ng g c new-face-snap
Ajoutez une nouvelle route create
à votre application :
//...
const routes: Routes = [
{ path: 'facesnaps/:id', component: SingleFaceSnapComponent },
{ path: 'facesnaps', component: FaceSnapListComponent },
{ path: 'create', component: NewFaceSnapComponent },
{ path: '', component: LandingPageComponent }
];
//...
Je vous propose de créer un bouton dans HeaderComponent pour la création des nouveaux FaceSnaps. Commencez par injecter le Router et par créer la méthode qui effectuera la redirection :
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss']
})
export class HeaderComponent implements OnInit {
constructor(private router: Router) { }
ngOnInit(): void {
}
onAddNewFaceSnap() {
this.router.navigateByUrl('/create');
}
}
Puis ajoutez le bouton au template :
<header>
<h1>snapface</h1>
<nav>
<a routerLink="" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<a routerLink="facesnaps" routerLinkActive="active">FaceSnaps</a>
</nav>
<button (click)="onAddNewFaceSnap()">+</button>
</header>
Un clic sur votre nouveau bouton affiche bien "new-face-snap works!".
Créez le formulaire
Tout d'abord, vous allez déclarer la variable qui contiendra l'objet du formulaire. Son type est FormGroup
(et non NgForm
comme pour les formulaires template) :
import { Component, OnInit } from '@angular/core';
import { FormGroup } from '@angular/forms';
@Component({
selector: 'app-new-face-snap',
templateUrl: './new-face-snap.component.html',
styleUrls: ['./new-face-snap.component.scss']
})
export class NewFaceSnapComponent implements OnInit {
snapForm!: FormGroup;
constructor() { }
ngOnInit(): void {
}
}
Ensuite, vous allez injecter un outil qui simplifie largement la génération des formulaires réactifs, le FormBuilder
:
//...
import { FormBuilder, FormGroup } from '@angular/forms';
//...
constructor(private formBuilder: FormBuilder) { }
//...
Puis, dans ngOnInit()
, vous allez utiliser le FormBuilder
pour construire votre formulaire :
ngOnInit(): void {
this.snapForm = this.formBuilder.group({
title: [null],
description: [null],
imageUrl: [null],
location: [null]
});
}
Vous utilisez la méthode group
du FormBuilder, en lui passant un objet :
les clés de l'objet correspondent aux noms des champs – ici, j'ai choisi les quatre champs du modèle FaceSnap que l'utilisateur pourra entrer ;
les valeurs de l'objet correspondent à la configuration de chaque champ – pour l'instant, vous passez uniquement
null
pour dire que la valeur par défaut de ces champs estnull
.
Créez dès maintenant une première version de la méthode qui enverra le contenu du formulaire :
onSubmitForm() {
console.log(this.snapForm.value);
}
Comme avec les formulaires template, vous accédez au contenu du formulaire avec l'attribut value
.
Branchez le formulaire
Il est temps maintenant de créer le formulaire dans le template et de le brancher à snapForm
. Je vous propose de mettre le formulaire dans une "card" comme celles des FaceSnaps. Dans new-face-snap.component.scss
:
.form-card {
box-sizing: border-box;
width: 50%;
margin: 20px auto;
padding: 10px 30px;
box-shadow: lightgray 4px 4px 20px;
}
.form-group {
margin: 10px auto;
width: 80%;
display: flex;
align-items: center;
justify-content: space-between;
}
input, textarea {
width: 50%;
}
.action-buttons {
width: 100%;
}
button {
display: block;
margin: 20px auto;
}
Et maintenant, dans le template :
<div class="form-card">
<h2>NOUVEAU FACESNAP</h2>
<form [formGroup]="snapForm">
</form>
</div>
Il faut attribuer un formControlName
à chaque input que vous ajouterez à ce formulaire : ces noms doivent correspondre aux noms des contrôles créés avec FormBuilder :
<form [formGroup]="snapForm">
<div class="form-group">
<label for="title">Titre</label>
<input id="title" type="text" formControlName="title">
</div>
<div class="form-group">
<label for="description">Description</label>
<textarea id="description" type="text" formControlName="description" rows="4">
</textarea>
</div>
<div class="form-group">
<label for="imageUrl">URL de l'image</label>
<input id="imageUrl" type="text" formControlName="imageUrl">
</div>
<div class="form-group">
<label for="location">Lieu</label>
<input id="location" type="text" formControlName="location">
</div>
<div class="action-buttons">
<button type="submit" (click)="onSubmitForm()">Enregistrer</button>
</div>
</form>
Ainsi, si vous remplissez le formulaire et cliquez sur Enregistrer :
Observez le formulaire
Pour avoir un premier aperçu du côté "réactif" des formulaires réactifs, je vous propose d'afficher en temps réel le FaceSnap que l'utilisateur est en train de créer.
Commencez par ajouter ces styles de NewFaceSnapComponent (ajoutez .face-snap-card
à la première ligne et ajoutez la classe .face-snap-card
en dessous, tout en laissant les autres styles du fichier en place) :
.form-card, .face-snap-card {
box-sizing: border-box;
width: 50%;
margin: 20px auto;
padding: 10px 30px;
box-shadow: lightgray 4px 4px 20px;
}
.face-snap-card {
img {
width: 100%;
}
h2 {
margin-bottom: 0;
}
p {
font-weight: 300;
font-size: 16px;
}
}
//...
Dans le TypeScript, vous allez créer un Observable faceSnapPreview$
qui émettra des objets de type FaceSnap
:
//...
snapForm!: FormGroup;
faceSnapPreview$!: Observable<FaceSnap>;
//...
Branchez cet Observable aux changements de valeur du formulaire avec son attribut valueChanges
, un Observable qui émet la valeur du formulaire à chaque modification :
this.faceSnapPreview$ = this.snapForm.valueChanges;
Le seul souci ici est que le formulaire n'émet pas des objets de type FaceSnap : il manque des attributs. Il faut donc utiliser l'un des opérateurs que vous avez découverts pour transformer les émissions en FaceSnaps valables – l'opérateur map()
:
this.faceSnapPreview$ = this.snapForm.valueChanges.pipe(
map(formValue => ({
...formValue,
createdDate: new Date(),
snaps: 0,
id: 0
}))
);
Vous pouvez maintenant utiliser le pipe async
pour afficher cet aperçu dans le template :
<div class="form-card">
<!-- ... -->
</div>
<div class="face-snap-card" *ngIf="faceSnapPreview$ | async as faceSnap">
<h2>{{ faceSnap.title | uppercase }}</h2>
<p>Mis en ligne {{ faceSnap.createdDate | date: 'à HH:mm, le d MMMM yyyy' }}</p>
<img [src]="faceSnap.imageUrl" [alt]="faceSnap.title">
<p>{{ faceSnap.description }}</p>
<p *ngIf="faceSnap.location">Photo prise à {{ faceSnap.location }}</p>
</div>
Vous voyez ici un pattern très courant et extrêmement utile qui utilise la directive *ngIf
, le pipe async
, et le mot-clé as
. Cette approche :
souscrit à l'Observable ;
ajoute la
<div>
uniquement lorsque l'Observable émet ;crée un alias pour l'émission qui est utilisable à l'intérieur de la
<div>
.
Cet alias permet de traiter les émissions de l'Observable comme si elles étaient des valeurs fixes. C'est ce qui vous permet d'accéder facilement aux attributs du FaceSnap émis sans avoir à y souscrire de nouveau.
Avec tout ça mis en place, lorsque vous remplissez le formulaire, vous voyez l'aperçu se créer devant vos yeux :
Il s'agit d'un exemple très simple, mais qui vous donne une première idée des possibilités liées aux formulaires réactifs.
En résumé
Ajoutez ReactiveFormsModule aux imports d'AppModule pour débloquer les formulaires réactifs ;
Utilisez FormBuilder pour générer un objet de type FormGroup ;
Liez le
form
du template au FormGroup avec[formGroup]
, et lesinput
du formulaire aux contrôles du FormGroup avecformControlName
;Observez les changements de valeur du formulaire avec son Observable
valueChanges
.
Avant de permettre aux utilisateurs d'ajouter leurs FaceSnaps, il faut valider le contenu du formulaire, et ce sera le sujet du prochain chapitre !