Appearance
CanDeactivateFn dans Angular : que faire quand on quitte une page ?
En une phrase : CanDeactivate
est un « garde » du routeur Angular qui vous permet d'intercepter la navigation avant que l'utilisateur ne quitte une page, afin de lui demander confirmation si des changements n'ont pas été sauvegardés.
Pourquoi s'y intéresser ?

Imaginez : vous êtes en train de remplir un long formulaire sur une application web. Vous avez passé plusieurs minutes à saisir des informations importantes. Soudain, un clic malencontreux sur le bouton 'Accueil' ou un lien externe, et voilà, tout votre travail s'envole en fumée... 😱 Frustrant, n'est-ce pas ?
C'est précisément pour éviter ce genre de scénario catastrophe que le garde CanDeactivateFn
d'Angular a été conçu. En l'implémentant, vous pouvez intercepter la tentative de l'utilisateur de quitter la page et lui demander une confirmation si des modifications n'ont pas été sauvegardées. Cela permet non seulement d'éviter la perte de données, mais aussi d'offrir une expérience utilisateur beaucoup plus professionnelle et rassurante.
Dans ce tutoriel, nous allons explorer ensemble, pas à pas, comment mettre en place et utiliser CanDeactivateFn
dans vos applications Angular.
Pré-requis express
Pour suivre ce tutoriel dans les meilleures conditions, il est recommandé de :
- Connaître les bases d'Angular (composants, services, réactivité de base).
- Savoir déclarer des routes avec le
RouterModule
d'Angular. - Avoir installé Angular 16 ou une version ultérieure (les exemples utilisent les gardes fonctionnels, généralisés depuis Angular 15 et devenus la norme).
Si ces termes vous semblent nouveaux, nous vous conseillons de jeter un œil au tutoriel officiel Tour of Heroes d'Angular avant de revenir ici. 😉
Qu'est-ce qu'un "garde" de navigation ?
Dans Angular, les gardes de navigation (ou "Route Guards" en anglais) sont des services ou des fonctions qui permettent de contrôler l'accès aux routes de votre application. Ils agissent comme des portiers qui décident si une navigation peut avoir lieu ou non, en se basant sur une logique que vous définissez.
Angular propose plusieurs types de gardes, chacun s'exécutant à un moment différent du cycle de navigation :
Garde | Moment où il s'exécute |
---|---|
CanMatch | Pour filtrer dynamiquement les correspondances de route |
CanActivate | Avant d'activer (entrer dans) une route |
CanActivateChild | Avant d'activer une route enfant |
CanDeactivate | Avant de désactiver (quitter) une route |
Resolve | Pour pré-charger des données avant l'activation d'une route |
CanLoad | Avant de charger un module en lazy-loading (déprécié) |
Dans ce tutoriel, nous allons nous concentrer exclusivement sur CanDeactivateFn
.
Signature de CanDeactivateFn<T>
La signature d'un garde CanDeactivateFn
est la suivante :
typescript
export type CanDeactivateFn<T> = (
component: T, // Le composant que l'on s'apprête à quitter
currentRoute: ActivatedRouteSnapshot,
currentState: RouterStateSnapshot,
nextState: RouterStateSnapshot
) => Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree;
Décortiquons cela :
component: T
: C'est l'instance du composant que l'utilisateur essaie de quitter.currentRoute: ActivatedRouteSnapshot
: Un instantané de la route actuellement active.currentState: RouterStateSnapshot
: L'état actuel du routeur.nextState: RouterStateSnapshot
: L'état du routeur vers lequel l'utilisateur tente de naviguer.
La fonction peut retourner :
boolean
:true
pour autoriser la navigation,false
pour l'annuler.UrlTree
: Pour rediriger l'utilisateur vers une autre URL.Observable<boolean | UrlTree>
ouPromise<boolean | UrlTree>
: Pour une décision asynchrone (par exemple, attendre la réponse d'une boîte de dialogue).
Le garde CanDeactivateFn
reçoit quatre paramètres importants :
Paramètre | Type | Description |
---|---|---|
component | T | Le composant actuel depuis lequel l'utilisateur tente de naviguer. T est le type de ce composant. |
currentRoute | ActivatedRouteSnapshot | Un instantané de la route active actuelle, contenant ses paramètres, données et autres informations. |
currentState | RouterStateSnapshot | Un instantané de l'état actuel du routeur, contenant l'URL complète et l'arbre d'URLs. |
nextState | RouterStateSnapshot | Un instantané de l'état du routeur vers lequel l'utilisateur tente de naviguer, incluant la future URL. |
Ces paramètres donnent au garde toutes les informations nécessaires pour prendre une décision éclairée sur l'autorisation ou non de la navigation.
Mise en place pas à pas
Suivons les étapes pour implémenter notre garde CanDeactivateFn
.
Étape 1 – Définir un contrat clair pour vos composants
Pour que notre garde puisse interagir avec n'importe quel composant susceptible d'avoir des modifications non sauvegardées, il est bon de définir une interface. Cette interface servira de "contrat" que les composants devront respecter.
typescript
import { Observable } from 'rxjs';
export interface ComponentCanDeactivate {
/**
* Cette méthode est appelée par le garde pour déterminer si la navigation hors du composant est autorisée.
* Elle doit retourner :
* - `true` si la navigation est autorisée.
* - `false` si la navigation doit être empêchée (ex : modifications non sauvegardées et l'utilisateur annule).
* - Un `Observable<boolean>` ou `Promise<boolean>` pour les vérifications asynchrones (ex : une boîte de dialogue de confirmation).
* @example
* ```typescript
* canDeactivate(): boolean {
* if (this.form.dirty && !this.formSubmitted) {
* return confirm('Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir quitter ?');
* }
* return true;
* }
* ```
*/
canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}
BONNE PRATIQUE
Vous pouvez enrichir cette interface si besoin (par exemple, ajouter une propriété pour un message de confirmation personnalisé), mais il est souvent préférable de la garder minimaliste au début.
Étape 2 – Créer le garde fonctionnel
Maintenant, créons notre garde pendingChangesGuard
. Ce garde sera une fonction qui respecte la signature CanDeactivateFn
.
Le principe est simple : le garde ne décide pas lui-même si la navigation est bloquée. Il délègue cette décision au composant concerné en appelant sa méthode canDeactivate()
typescript
import { CanDeactivateFn } from '@angular/router';
import { ComponentCanDeactivate } from '../interfaces/component-can-deactivate.interface';
import { Observable } from 'rxjs';
/**
* Un garde CanDeactivate qui vérifie si un composant peut être désactivé.
* Il s'appuie sur le composant implémentant l'interface `ComponentCanDeactivate`.
* Si le composant n'implémente pas `canDeactivate`, la navigation est autorisée.
* Sinon, il appelle la méthode `canDeactivate` du composant.
* Si `canDeactivate` retourne un booléen `false`, il demande à l'utilisateur avec une boîte de dialogue de confirmation générique.
*/
export const pendingChangesGuard: CanDeactivateFn<ComponentCanDeactivate> = (
component: ComponentCanDeactivate,
// Les paramètres currentRoute, currentState, et nextState ne sont pas utilisés ici,
// donc on peut les préfixer avec _ pour indiquer qu'ils sont intentionnellement ignorés.
_currentRoute,
_currentState,
_nextState
) => {
// 1. Si le composant n'implémente pas la méthode canDeactivate (par exemple, il n'a rien à protéger),
// on autorise la navigation.
if (!component.canDeactivate) {
return true;
}
// 2. On délègue la décision au composant en appelant sa méthode canDeactivate().
const result = component.canDeactivate();
// 3. Si le résultat est un booléen (décision synchrone)
if (typeof result === 'boolean') {
// Si result est true, on navigue. Si c'est false, on affiche la confirmation.
// L'opérateur || (OU logique) fait que si `result` est `true`, l'expression entière est `true`.
// Si `result` est `false`, alors `confirm(...)` est exécuté et sa valeur (true/false) est retournée.
return result || confirm('Vous avez des modifications non sauvegardées. Voulez-vous vraiment quitter cette page ?');
}
// 4. Si le résultat est un Observable ou une Promise (décision asynchrone),
// le routeur attendra que l'Observable/Promise se résolve.
// Le composant est responsable d'afficher une éventuelle boîte de dialogue personnalisée.
return result;
};
Étape 3 – Enregistrer le garde dans le routeur
Pour que notre garde soit actif sur une route spécifique, nous devons le déclarer dans la configuration des routes.
typescript
import { Routes } from '@angular/router';
import { UserFormComponent } from './components/user-form/user-form.component'; // Assurez-vous que le chemin est correct
import { pendingChangesGuard } from './guards/pending-changes.guard';
export const routes: Routes = [
{
path: 'users/edit/:id',
component: UserFormComponent,
canDeactivate: [pendingChangesGuard] // On applique notre garde ici !
},
// ... autres routes de votre application
];
Étape 4 – Implémenter canDeactivate()
côté composant
C'est ici que la logique de vérification des changements non sauvegardés prend place. Notre composant UserFormComponent
(par exemple, un formulaire d'édition d'utilisateur) doit implémenter l'interface ComponentCanDeactivate
.
typescript
import { Component, inject } from '@angular/core';
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { ComponentCanDeactivate } from '../../interfaces/component-can-deactivate.interface';
import { Observable, tap } from 'rxjs';
import { AsyncPipe, CommonModule } from '@angular/common';
import { UserService } from '../../services/user.service'; // Adaptez le chemin si nécessaire
import { User } from '../../interfaces/user.interface';
@Component({
selector: 'app-user-form',
standalone: true,
imports: [ReactiveFormsModule, AsyncPipe, CommonModule],
template: `
@if (user$ | async; as user) {
<form [formGroup]="userForm" (ngSubmit)="saveUser()">
<h2>Modifier l'utilisateur {{ user.name }}</h2>
<div>
<label for="name">Nom:</label>
<input type="text" id="name" formControlName="name">
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" formControlName="email">
</div>
<div>
<label for="username">Nom d'utilisateur:</label>
<input type="text" id="username" formControlName="username">
</div>
<button type="submit" [disabled]="userForm.invalid || !userForm.dirty">Enregistrer</button>
</form>
@if (userForm.dirty && !formSubmitted) {
<p style="color: orange;">Attention, vous avez des modifications non sauvegardées !</p>
}
} @else {
<p>Chargement des données de l'utilisateur...</p>
}
`
})
export class UserFormComponent implements ComponentCanDeactivate {
private formBuilder = inject(FormBuilder);
private userService = inject(UserService);
userForm: FormGroup;
private originalUser: User | null = null;
formSubmitted = false;
// Simule l'ID de l'utilisateur, normalement obtenu des paramètres de la route (e.g., ActivatedRoute)
private userId = 1;
// Observable pour charger les données de l'utilisateur et initialiser le formulaire
user$: Observable<User> = this.userService.getUser(this.userId).pipe(
tap(user => {
this.originalUser = { ...user }; // Stocke une copie pour comparaison
this.userForm.patchValue(user);
// Après avoir initialisé le formulaire avec les données, on le marque comme "pristine"
// car ces changements ne viennent pas de l'utilisateur.
this.userForm.markAsPristine();
this.formSubmitted = false; // Réinitialise au cas où on recharge les données
})
);
userForm = this.formBuilder.group({
id: [null],
name: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
username: ['', Validators.required]
});
/**
* Sauvegarde les données de l'utilisateur.
* Typiquement, cela impliquerait un appel API.
* Après une sauvegarde réussie, marquer le formulaire comme pristine et soumis.
*/
saveUser() {
if (this.userForm.valid) {
console.log('Sauvegarde de l\'utilisateur :', this.userForm.value);
// Simule un appel API
this.userService.saveUser(this.userForm.value).subscribe(() => {
this.formSubmitted = true;
this.userForm.markAsPristine(); // Important après la sauvegarde !
alert('Utilisateur sauvegardé !');
// Mettre à jour originalUser si nécessaire ou recharger les données
this.originalUser = { ...this.userForm.value };
});
}
}
/**
* Appelé par le `pendingChangesGuard`.
* Détermine si l'utilisateur peut naviguer hors du composant.
* @returns `true` si la navigation est autorisée, `false` ou un `Observable<boolean>` sinon.
* @example
* ```typescript
* // Cette logique est maintenant utilisée dans le garde lui-même si un booléen false est retourné.
* // if (this.userForm.dirty && !this.formSubmitted) {
* // return confirm('Vous avez des modifications non sauvegardées. Êtes-vous sûr de vouloir quitter ?');
* // }
* // return true;
* ```
*/
canDeactivate(): boolean {
// Si le formulaire a été soumis, ou s'il n'a pas été modifié (il est "pristine"),
// alors l'utilisateur peut naviguer sans confirmation.
if (this.formSubmitted || !this.userForm.dirty) {
return true;
}
// Sinon (formulaire modifié et non soumis), le garde demandera confirmation.
return false;
}
}
Comprendre pristine
, dirty
, et markAsPristine()
Dans les formulaires réactifs d'Angular, chaque contrôle et le formulaire lui-même ont des états qui décrivent leur interaction avec l'utilisateur :
pristine
: Vrai si l'utilisateur n'a pas encore modifié la valeur du contrôle/formulaire depuis son initialisation. Faux dès la première modification.dirty
: L'inverse depristine
. Vrai si l'utilisateur a modifié la valeur.
Lorsqu'on charge des données existantes dans un formulaire (par exemple, les informations d'un utilisateur à éditer) avec patchValue()
ou setValue()
, Angular considère ces actions comme des modifications et marque automatiquement le formulaire comme dirty
.
Cependant, du point de vue de l'utilisateur, le formulaire vient juste d'être initialisé avec des données préexistantes ; il n'a pas encore fait de nouvelles modifications. C'est pourquoi il est crucial d'appeler this.userForm.markAsPristine()
juste après avoir rempli le formulaire avec les données initiales. Cela réinitialise son état à pristine
, indiquant qu'il est dans son état "propre" initial.
De même, après une sauvegarde réussie, il est bon de marquer le formulaire comme pristine
car les modifications ont été persistées.
La méthode canDeactivate()
de notre composant utilise cette logique :
this.formSubmitted
: Si l'utilisateur vient de cliquer sur "Enregistrer" et que la sauvegarde a réussi, il peut partir.!this.userForm.dirty
: Si le formulaire n'a pas été touché ou si les modifications ont été sauvegardées (et donc marqué commepristine
), il peut partir.- Si aucune de ces conditions n'est vraie (le formulaire est
dirty
et nonsubmitted
), alorscanDeactivate()
retournefalse
, et notrependingChangesGuard
affichera la boîte de dialogueconfirm()
.
Résultat : si l'utilisateur a changé quelque chose sans sauvegarder, il reçoit une alerte du navigateur lui demandant de confirmer son départ.
Aller plus loin : boîte de dialogue personnalisée
La fonction window.confirm()
est simple, mais son apparence n'est pas personnalisable et peut jurer avec le design de votre application. Pour une expérience utilisateur plus soignée, vous pouvez utiliser une boîte de dialogue modale personnalisée (par exemple, avec Angular Material Dialog, ng-bootstrap Modal, ou votre propre composant de dialogue).
Pour cela, votre méthode canDeactivate()
dans le composant devra retourner un Observable<boolean>
ou une Promise<boolean>
.
Voici l'idée générale (l'implémentation exacte dépendra de votre service de dialogue) :
typescript
// ... autres imports
import { DialogService } from '../../services/dialog.service'; // Votre service de dialogue personnalisé
import { Subject } from 'rxjs';
// @Component(...
export class UserFormAdvancedComponent implements ComponentCanDeactivate {
// ... (propriétés du formulaire, userService, etc.)
private dialogService = inject(DialogService);
// ... (constructor, ngOnInit, saveUser)
/**
* Vérifie si le composant peut être désactivé.
* S'il y a des modifications non sauvegardées, il ouvre une boîte de dialogue de confirmation personnalisée.
* @returns `true` s'il n'y a pas de modifications non sauvegardées, ou un `Observable<boolean>` qui se résout en fonction du choix de l'utilisateur dans la boîte de dialogue.
*/
canDeactivate(): Observable<boolean> | boolean {
if (this.formSubmitted || !this.userForm.dirty) {
return true; // Pas de changements ou déjà sauvegardé, navigation OK
}
// Utilise un Subject pour retourner un Observable que le garde attendra.
const confirmationSubject = new Subject<boolean>();
this.dialogService.confirm(
'Modifications non sauvegardées',
'Voulez-vous vraiment quitter cette page ? Toutes les modifications non enregistrées seront perdues.',
() => { // Action si l'utilisateur confirme (Oui)
confirmationSubject.next(true);
confirmationSubject.complete();
},
() => { // Action si l'utilisateur annule (Non)
confirmationSubject.next(false);
confirmationSubject.complete();
}
);
return confirmationSubject.asObservable();
}
}
Dans ce cas, le pendingChangesGuard
recevra cet Observable
et attendra sa résolution avant de permettre ou d'empêcher la navigation.
Migrer d'un garde de classe existant
Si vous avez un projet plus ancien utilisant des gardes basés sur des classes (qui implémentent l'interface CanDeactivate
), Angular fournit un utilitaire mapToCanDeactivate
pour faciliter la transition vers les gardes fonctionnels sans réécriture immédiate.
typescript
import { Routes, mapToCanDeactivate } from '@angular/router';
import { UserFormComponent } from './components/user-form/user-form.component';
import { LegacyPendingChangesGuard } from './guards/legacy-pending-changes.guard'; // Votre ancien garde de classe
export const routes: Routes = [
{
path: 'users/edit/:id',
component: UserFormComponent,
// On mappe l'ancien garde de classe vers un format compatible avec les gardes fonctionnels
canDeactivate: mapToCanDeactivate([LegacyPendingChangesGuard])
},
];
DÉPRÉCIATION DES GARDES DE CLASSE
Bien que mapToCanDeactivate
soit utile pour la transition, les gardes basés sur des classes sont officiellement dépréciés depuis Angular 15. Il est fortement recommandé de migrer votre logique vers des gardes purement fonctionnels dès que possible pour bénéficier de la simplicité et des performances améliorées.
FAQ express
Q : Puis-je protéger plusieurs composants avec le même garde pendingChangesGuard
?
R : Oui, absolument ! Tant que chaque composant que vous souhaitez protéger implémente correctement l'interface ComponentCanDeactivate
(et donc sa propre logique canDeactivate()
), le même garde pendingChangesGuard
peut être réutilisé sur plusieurs routes.
Q : Et si je veux rediriger vers une autre page spécifique plutôt que de simplement bloquer/confirmer ?
R : Votre garde (ou la méthode canDeactivate()
du composant si elle retourne un Observable
) peut retourner un UrlTree
. Par exemple : return router.parseUrl('/confirmation-abandon');
(vous devrez injecter Router
pour utiliser parseUrl
).
Q : window.confirm()
est un peu basique. Comment puis-je l'améliorer visuellement ?
R : Comme mentionné dans la section "Aller plus loin", retournez un Observable<boolean>
ou Promise<boolean>
depuis la méthode canDeactivate()
de votre composant. Dans cette méthode, vous déclenchez l'affichage de votre propre composant de dialogue (par exemple, un MatDialog
d'Angular Material). L'Observable/Promise se résoudra en true
ou false
selon le choix de l'utilisateur dans votre dialogue personnalisé.