Pipes, formulaires réactifs et état partagé
Transformer l'affichage, construire des formulaires solides, partager l'état.
Objectifs
À la fin de cette leçon, vous saurez :
- utiliser les pipes Angular pour formater l'affichage sans toucher aux données ;
- construire un formulaire réactif avec validation ;
- distinguer état local de composant et état partagé, et partager via un service à signal ;
- consolider le projet todo-list avec ces trois pièces.
🔗 Pour vous rafraîchir la mémoire : décorateurs et metadata · les bases TypeScript · modules : import/export
Les pipes : transformer pour l'affichage
Un pipe est une fonction appliquée dans le template avec l'opérateur | : il change la présentation, jamais la donnée.
export class TachesComponent {
echeance = new Date(2026, 7, 25);
prix = 1290.5;
}
<p>{{ echeance | date:"dd/MM/yyyy" }}</p> <!-- 25/08/2026 -->
<p>{{ prix | currency:"EUR":"symbol" }}</p> <!-- 1 290,50 € -->
<p>{{ titre | uppercase }}</p>
La donnée reste un Date et un nombre dans le composant : seuls le format français et le symbole monétaire apparaissent. Le pipe async mérite une mention spéciale : il s'abonne à un flux (Promise/Observable), affiche sa dernière valeur et se désabonne tout seul — plus de gestion manuelle.
Un pipe personnalisé est une classe décorée @Pipe :
@Pipe({ name: "resume" })
export class ResumePipe implements PipeTransform {
transform(valeur: string, max = 40): string {
return valeur.length <= max ? valeur : valeur.slice(0, max) + "…";
}
}
<span>{{ tache.description | resume:60 }}</span>
Le formulaire réactif : le formulaire sous contrat
Angular propose deux approches ; le cursus retient les formulaires réactifs, explicites et testables — dans la ligne de tout ce que vous avez appris.
import { Component, inject } from "@angular/core";
import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms";
@Component({
selector: "app-nouvelle-tache",
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="formulaire" (ngSubmit)="enregistrer()">
<input formControlName="titre" placeholder="Titre" />
<button [disabled]="formulaire.invalid">Ajouter</button>
@if (formulaire.controls.titre.hasError("required")) {
<small>Le titre est obligatoire.</small>
}
</form>
`,
})
export class NouvelleTacheComponent {
private fb = inject(FormBuilder);
formulaire = this.fb.group({
titre: ["", [Validators.required, Validators.minLength(3)]],
priorite: ["normale"],
});
enregistrer() {
if (this.formulaire.invalid) return;
console.log(this.formulaire.getRawValue()); // { titre: "...", priorite: "..." }
this.formulaire.reset();
}
}
À lire attentivement :
- le formulaire existe dans la classe (
FormGroup), pas seulement dans le HTML : on peut le tester comme n'importe quel objet ; - chaque contrôle porte ses validateurs ;
invalid/hasErrorpilotent l'UI ; (ngSubmit)remplace(submit)et respectepreventDefaultpour vous ;getRawValue()rend des données typées prêtes pour le service HTTP.
C'est exactement votre validation « côté front » du chapitre 16 — mais déclarée au lieu d'être écrite à la main.
L'état : local, puis partagé
Jusqu'ici chaque composant gardait ses données en propriétés internes : c'est l'état local. Il suffit tant que rien d'autre ne lit ces données. Le jour où deux pages doivent voir les mêmes tâches (la liste et le détail), il faut un état partagé — et vous savez déjà où le mettre : dans un service injectable.
import { Injectable, signal } from "@angular/core";
export interface Tache { id: number; titre: string; fait: boolean; }
@Injectable({ providedIn: "root" })
export class TachesStore {
// signal : valeur réactive qui prévient ses lecteurs quand elle change
readonly taches = signal<Tache[]>([]);
charger(initial: Tache[]) { this.taches.set(initial); }
basculer(id: number) {
this.taches.update((liste) =>
liste.map((t) => (t.id === id ? { ...t, fait: !t.fait } : t)),
);
}
ajouter(titre: string) {
this.taches.update((liste) => [...liste, { id: Date.now(), titre, fait: false }]);
}
}
Dans un composant, lire le signal dans le template suffit à rester synchronisé :
@for (t of store.taches(); track t.id) {
<li (click)="store.basculer(t.id)">{{ t.titre }}</li>
}
Le service est unique (providedIn: "root"), donc toute modification faite par un composant est visible des autres — fini la liste qui « oublie » qu'une tâche a été créée ailleurs.
Consolidation du projet todo
Les exercices précédents ont reconstruit la todo par morceaux (composant, service HTTP, création). La version complète assemble :
TachesStoredétient la liste (état partagé).- Au démarrage, un effet/appel HTTP charge les tâches depuis votre API NestJS →
store.charger(...). - Le formulaire réactif crée les tâches →
store.ajouter(...)puis POST. - Le clic bascule
fait→store.basculer(...)puis PATCH. - Les pipes affichent dates et statuts.
Chaque brique a déjà été pratiquée isolément ; ce projet est leur première répétition générale avant Angular avancé.
Exercice
- Affichez la date de création d'une tâche au format
dd/MM/yy HH:mmavec un pipe. - Ajoutez au formulaire un champ
prioritelimité à trois valeurs (basse,normale,haute) viaValidators.pattern. - Créez
TachesStoreet faites passerbasculer()par lui dans votre composant liste. - Ouvrez deux routes différentes utilisant le même store : vérifiez que créer une tâche sur l'une met à jour l'autre.
- Écrivez
TronquerPipelimitant les descriptions à N caractères.
Résumé
- Un pipe transforme l'affichage ;
asyncgère abonnement/désabonnement. - Le formulaire réactif vit dans la classe : validation déclarative et testable.
- État local pour un composant, service à signaux pour l'état partagé.
- Le projet todo devient : store + formulaire réactif + HttpClient + pipes.
Correction disponibleCherchez d’abord par vous-même.Voir la correction
Correction
Réponses détaillées
Question 1. {{ t.creeLe | date:"dd/MM/yy HH:mm" }} — la propriété reste un Date.
Question 2.
priorite: ["normale", [Validators.required, Validators.pattern(/^(basse|normale|haute)$/)]],
Toute autre valeur rend formulaire.invalid, donc désactive le bouton.
Question 3. Injecter private store = inject(TachesStore); puis appeler store.basculer(t.id) au clic ; retirer la mutation locale du tableau du composant.
Question 4. Les deux routes reçoivent la même instance du service (providedIn: "root"). Le signal notifie tous ses lecteurs : la liste de l'autre route reflète l'ajout dès qu'on y revient (et immédiatement si elle est affichée).
Question 5.
@Pipe({ name: "tronquer" })
export class TronquerPipe implements PipeTransform {
transform(valeur: string | null, max = 30): string {
if (!valeur) return "";
return valeur.length <= max ? valeur : valeur.slice(0, max - 1) + "…";
}
}
Ne pas oublier d'ajouter le pipe aux imports du composant qui l'utilise.