Skip to content

Vous souhaitez recevoir de l'aide sur ce sujet ? rejoignez la communauté Angular.fr sur Discord.

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/cdk

Qu'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 code

Exemple 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.