Appearance
Créer des boîtes de dialogue avec CDK Dialog
Dans ce tutoriel, nous allons découvrir comment utiliser CDK Dialog pour créer des boîtes de dialogue (modales) dans vos applications Angular. CDK Dialog offre une solution flexible et accessible pour afficher des contenus dans des overlays, sans imposer de style visuel spécifique.
Analogie du monde réel
Imaginez que vous êtes dans un restaurant et que vous souhaitez commander un plat spécial. Le serveur vous apporte un menu supplémentaire dans une petite table roulante qui se place devant vous - c'est exactement ce qu'est une boîte de dialogue : un contenu qui apparaît par-dessus votre interface principale pour capturer votre attention et vous permettre d'interagir avec, avant de disparaître une fois l'interaction terminée.
CDK Dialog est l'outil qui vous permet de créer ces "tables roulantes" dans vos applications web, de manière élégante et accessible.
Installation d'Angular CDK
Commençons par installer Angular CDK :
bash
ng add @angular/cdkQu'est-ce que Angular CDK ?
Angular CDK est un ensemble d'outils de développement qui fournit des composants réutilisables et des services pour créer des fonctionnalités complexes dans les applications Angular. Il offre des solutions pour la gestion des overlays, du drag & drop, des tables virtuelles, de l'accessibilité et bien d'autres fonctionnalités, sans imposer de style visuel spécifique. C'est la base sur laquelle Angular Material est construit, mais il peut être utilisé indépendamment pour créer vos propres composants personnalisés.
CDK Dialog fonctionne directement sans configuration supplémentaire. Le service Dialog peut être injecté dans n'importe quel composant ou service pour ouvrir des dialogues.
Créer un composant de dialogue
Un composant de dialogue est simplement un composant Angular standard. Créons un composant pour confirmer la suppression d'un utilisateur :
typescript
import { Component, input, output } from '@angular/core';
import { DialogRef } from '@angular/cdk/dialog';
@Component({
selector: 'app-confirm-dialog',
template: `
<div class="dialog-container">
<h2>Confirmer la suppression</h2>
<p>Êtes-vous sûr de vouloir supprimer l'utilisateur <strong>{{ userName() }}</strong> ?</p>
<div class="dialog-actions">
<button (click)="onCancel()">Annuler</button>
<button (click)="onConfirm()" class="btn-danger">Supprimer</button>
</div>
</div>
`,
styles: [`
.dialog-container {
padding: 1.5rem;
min-width: 400px;
}
.dialog-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
.btn-danger {
background-color: #dc2626;
color: white;
}
`]
})
export class ConfirmDialogComponent {
// Utilisation des signaux pour recevoir les données
userName = input<string>('');
// Injection de DialogRef pour fermer le dialogue et retourner un résultat
private dialogRef = inject(DialogRef<boolean>);
/**
* Annule l'action et ferme le dialogue en retournant false
* Cette méthode est appelée lorsque l'utilisateur clique sur "Annuler"
*/
onCancel(): void {
this.dialogRef.close(false);
}
/**
* Confirme l'action et ferme le dialogue en retournant true
* Cette méthode est appelée lorsque l'utilisateur clique sur "Supprimer"
*/
onConfirm(): void {
this.dialogRef.close(true);
}
}DialogRef
DialogRef est injecté dans le composant de dialogue et permet de :
- Fermer le dialogue avec
close(result) - Accéder aux données passées lors de l'ouverture
- Écouter les événements de fermeture
Ouvrir un dialogue
Pour ouvrir un dialogue, injectez le service Dialog dans votre composant :
typescript
import { Component, inject } from '@angular/core';
import { Dialog } from '@angular/cdk/dialog';
import { ConfirmDialogComponent } from './confirm-dialog.component';
import { User } from './user.interface';
@Component({
selector: 'app-user',
template: `
<div class="user-list">
@for (user of users(); track user.id) {
<div class="user-item">
<span>{{ user.name }}</span>
<button (click)="deleteUser(user)">Supprimer</button>
</div>
}
</div>
`
})
export class UserComponent {
users = signal<User[]>([
{ id: 1, name: 'John Doe', username: 'johndoe', email: '[email protected]' },
{ id: 2, name: 'Jane Smith', username: 'janesmith', email: '[email protected]' }
]);
// Injection du service Dialog pour ouvrir des dialogues
private dialog = inject(Dialog);
/**
* Ouvre un dialogue de confirmation avant de supprimer un utilisateur
* Utilise DialogRef pour récupérer le résultat de la confirmation
*/
deleteUser(user: User): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { userName: user.name },
width: '500px',
disableClose: true
});
// Écoute le résultat du dialogue
dialogRef.closed.subscribe(result => {
if (result === true) {
// L'utilisateur a confirmé, on supprime l'utilisateur
this.users.update(users => users.filter(u => u.id !== user.id));
}
});
}
}Passer des données au dialogue
Il existe plusieurs façons de passer des données à un composant de dialogue. La méthode recommandée est d'utiliser la propriété data dans les options :
typescript
// Dans le composant qui ouvre le dialogue
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: {
userName: user.name,
userId: user.id
}
});Pour accéder à ces données dans le composant de dialogue, utilisez DIALOG_DATA :
typescript
import { Component, inject } from '@angular/core';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
interface DialogData {
userName: string;
userId: number;
}
@Component({
selector: 'app-confirm-dialog',
template: `
<div class="dialog-container">
<h2>Confirmer la suppression</h2>
<p>Êtes-vous sûr de vouloir supprimer l'utilisateur <strong>{{ data().userName }}</strong> ?</p>
<!-- ... -->
</div>
`
})
export class ConfirmDialogComponent {
// Injection des données passées au dialogue
data = signal(inject(DIALOG_DATA) as DialogData);
private dialogRef = inject(DialogRef<boolean>);
onConfirm(): void {
// Vous pouvez accéder à data().userId si nécessaire
this.dialogRef.close(true);
}
}Typage des données
Utilisez une interface TypeScript pour typer les données du dialogue. Cela garantit la sécurité de type et améliore l'expérience de développement.
Options de configuration
Le service Dialog accepte de nombreuses options pour personnaliser le comportement du dialogue :
typescript
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
// Dimensions
width: '500px',
height: '400px',
minWidth: '300px',
maxWidth: '90vw',
// Position
position: {
top: '50px',
left: '50%'
},
// Comportement
disableClose: true, // Empêche la fermeture en cliquant en dehors ou en appuyant sur Escape
hasBackdrop: true, // Affiche un fond sombre derrière le dialogue
backdropClass: 'custom-backdrop', // Classe CSS personnalisée pour le backdrop
// Données
data: { userName: 'John Doe' },
// Autres options
autoFocus: true, // Focus automatique sur le premier élément interactif
restoreFocus: true, // Restaure le focus sur l'élément qui a ouvert le dialogue
role: 'dialog', // Rôle ARIA (par défaut: 'dialog')
ariaDescribedBy: 'dialog-description', // ID de l'élément qui décrit le dialogue
ariaLabelledBy: 'dialog-title' // ID de l'élément qui étiquette le dialogue
});Récupérer le résultat du dialogue
Le dialogue peut retourner une valeur lors de sa fermeture. Utilisez closed pour écouter le résultat :
typescript
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { userName: user.name }
});
// Méthode 1 : Utiliser l'observable closed
dialogRef.closed.subscribe(result => {
if (result === true) {
console.log('Utilisateur confirmé');
}
});
// Méthode 2 : Utiliser toSignal pour convertir en signal (Angular 20+)
import { toSignal } from '@angular/core/rxjs-interop';
const result = toSignal(dialogRef.closed, { initialValue: false });
// Utiliser result() dans le template ou le codeExemple complet : Formulaire de création d'utilisateur
Créons un exemple plus complexe avec un formulaire dans le dialogue :
typescript
import { Component, inject, signal } from '@angular/core';
import { DialogRef, DIALOG_DATA } from '@angular/cdk/dialog';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
export interface UserFormData {
name: string;
email: string;
}
@Component({
selector: 'app-user-form-dialog',
imports: [ReactiveFormsModule],
template: `
<div class="dialog-container">
<h2>{{ isEditMode() ? 'Modifier' : 'Créer' }} un utilisateur</h2>
<form [formGroup]="userForm" (ngSubmit)="onSubmit()">
<div class="form-group">
<label for="name">Nom</label>
<input
id="name"
formControlName="name"
type="text"
[class.error]="userForm.get('name')?.invalid && userForm.get('name')?.touched"
/>
@if (userForm.get('name')?.invalid && userForm.get('name')?.touched) {
<span class="error-message">Le nom est requis</span>
}
</div>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
formControlName="email"
type="email"
[class.error]="userForm.get('email')?.invalid && userForm.get('email')?.touched"
/>
@if (userForm.get('email')?.invalid && userForm.get('email')?.touched) {
<span class="error-message">Un email valide est requis</span>
}
</div>
<div class="dialog-actions">
<button type="button" (click)="onCancel()">Annuler</button>
<button type="submit" [disabled]="userForm.invalid">
{{ isEditMode() ? 'Modifier' : 'Créer' }}
</button>
</div>
</form>
</div>
`,
styles: [`
.dialog-container {
padding: 1.5rem;
min-width: 400px;
}
.form-group {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
}
input.error {
border-color: #dc2626;
}
.error-message {
color: #dc2626;
font-size: 0.875rem;
margin-top: 0.25rem;
display: block;
}
.dialog-actions {
display: flex;
gap: 1rem;
justify-content: flex-end;
margin-top: 1.5rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
}
button[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
`]
})
export class UserFormDialogComponent {
private dialogRef = inject(DialogRef<UserFormData | null>);
private fb = inject(FormBuilder);
// Récupération des données (peut être undefined pour création)
data = signal(inject(DIALOG_DATA) as UserFormData | undefined);
// Détermine si on est en mode édition ou création
isEditMode = signal(!!this.data());
/**
* Création du formulaire réactif avec validation
* Utilise les données existantes si on est en mode édition
*/
userForm = this.fb.group({
name: [this.data()?.name || '', Validators.required],
email: [this.data()?.email || '', [Validators.required, Validators.email]]
});
/**
* Annule l'opération et ferme le dialogue sans retourner de données
*/
onCancel(): void {
this.dialogRef.close(null);
}
/**
* Soumet le formulaire et retourne les données si valides
* Cette méthode est appelée lors de la soumission du formulaire
*/
onSubmit(): void {
if (this.userForm.valid) {
this.dialogRef.close(this.userForm.value as UserFormData);
}
}
}Utilisation dans le composant parent :
typescript
import { Component, inject, signal } from '@angular/core';
import { Dialog } from '@angular/cdk/dialog';
import { UserFormDialogComponent, UserFormData } from './user-form-dialog.component';
import { User } from './user.interface';
@Component({
selector: 'app-user',
template: `
<div class="user-management">
<button (click)="openCreateDialog()">Créer un utilisateur</button>
<div class="user-list">
@for (user of users(); track user.id) {
<div class="user-item">
<span>{{ user.name }} - {{ user.email }}</span>
<button (click)="openEditDialog(user)">Modifier</button>
</div>
}
</div>
</div>
`
})
export class UserComponent {
users = signal<User[]>([
{ id: 1, name: 'John Doe', username: 'johndoe', email: '[email protected]' }
]);
private dialog = inject(Dialog);
private nextId = signal(2);
/**
* Ouvre le dialogue de création d'utilisateur
* Crée un nouvel utilisateur avec les données retournées
*/
openCreateDialog(): void {
const dialogRef = this.dialog.open(UserFormDialogComponent, {
width: '500px',
disableClose: true
});
dialogRef.closed.subscribe(result => {
if (result) {
const newUser: User = {
id: this.nextId(),
name: result.name,
username: result.email.split('@')[0],
email: result.email
};
this.users.update(users => [...users, newUser]);
this.nextId.update(id => id + 1);
}
});
}
/**
* Ouvre le dialogue d'édition d'utilisateur
* Met à jour l'utilisateur existant avec les données retournées
*/
openEditDialog(user: User): void {
const dialogRef = this.dialog.open(UserFormDialogComponent, {
width: '500px',
disableClose: true,
data: {
name: user.name,
email: user.email
}
});
dialogRef.closed.subscribe(result => {
if (result) {
this.users.update(users =>
users.map(u =>
u.id === user.id
? { ...u, name: result.name, email: result.email }
: u
)
);
}
});
}
}Bonnes pratiques
1. Accessibilité
Assurez-vous que vos dialogues sont accessibles :
typescript
@Component({
selector: 'app-accessible-dialog',
template: `
<div
role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Titre du dialogue</h2>
<p id="dialog-description">Description du dialogue</p>
<!-- ... -->
</div>
`
})
export class AccessibleDialogComponent {}2. Gestion du focus
CDK Dialog gère automatiquement le focus, mais vous pouvez le personnaliser :
typescript
const dialogRef = this.dialog.open(MyDialogComponent, {
autoFocus: 'first-heading', // Focus sur le premier élément avec role="heading"
restoreFocus: true // Restaure le focus sur l'élément qui a ouvert le dialogue
});3. Styles personnalisés
Créez un style cohérent pour tous vos dialogues :
typescript
// Dans votre styles globaux ou dans le composant
::ng-deep .cdk-overlay-container {
z-index: 1000;
}
::ng-deep .cdk-overlay-backdrop {
background-color: rgba(0, 0, 0, 0.5);
}
::ng-deep .cdk-overlay-pane {
border-radius: 0.5rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}4. Service de dialogue réutilisable
Créez un service pour centraliser la logique des dialogues :
typescript
import { Injectable, inject } from '@angular/core';
import { Dialog } from '@angular/cdk/dialog';
import { ConfirmDialogComponent } from './confirm-dialog.component';
import { Observable } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class DialogService {
private dialog = inject(Dialog);
/**
* Ouvre un dialogue de confirmation standardisé
* Retourne un observable qui émet true si confirmé, false sinon
*/
confirm(message: string, title: string = 'Confirmation'): Observable<boolean> {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '400px',
disableClose: true,
data: { message, title }
});
return dialogRef.closed.pipe(
map(result => result === true)
);
}
}5. Nettoyage des souscriptions
N'oubliez pas de nettoyer les souscriptions aux observables :
typescript
import { Component, inject, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs';
export class UserComponent implements OnDestroy {
private dialog = inject(Dialog);
private subscriptions = new Subscription();
deleteUser(user: User): void {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
data: { userName: user.name }
});
// Ajouter la souscription à la collection pour nettoyage automatique
this.subscriptions.add(
dialogRef.closed.subscribe(result => {
if (result === true) {
// Supprimer l'utilisateur
}
})
);
}
ngOnDestroy(): void {
this.subscriptions.unsubscribe();
}
}Alternative avec takeUntilDestroyed
Avec Angular 16+, utilisez takeUntilDestroyed pour un nettoyage automatique :
typescript
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
dialogRef.closed
.pipe(takeUntilDestroyed())
.subscribe(result => {
// ...
});Performance
Évitez d'ouvrir trop de dialogues simultanément. CDK Dialog gère automatiquement les overlays, mais trop de dialogues ouverts peuvent impacter les performances.
