Cours JavaScript et TypeScript · Module 8: Événements et DOM

Custom Events et dispatchEvent en JavaScript

7 min de lecture
Dans cette leçon9

« Écoute attentivement », dit le Dr Alan Grant, penché sur la console du système de surveillance. « Les dinosaures communiquent entre eux à l'aide de sons - des grondements graves aux rugissements terrifiants. Dans notre parc, on a besoin d'un système de communication similaire entre les modules. Et c'est là qu'interviennent les Custom Events ! »

Que sont les Custom Events ?

Jusqu'ici, on a découvert les événements DOM intégrés - click, keydown, submit et scroll. Mais JavaScript nous permet de créer nos propres événements non standard. Grâce à eux, on peut construire des systèmes de communication entre différentes parties d'une application, exactement comme le système d'alarme de Jurassic Park envoie des notifications à différentes zones.

Les Custom Events sont des événements définis par le programmeur qui peuvent transporter n'importe quelles données et être écoutés exactement comme les événements DOM ordinaires.

Le constructeur CustomEvent

Pour créer un événement personnalisé, on utilise le constructeur CustomEvent :

1// Création de base d'un Custom Event
2const alertEvent = new CustomEvent('dinoAlert');
3
4// Custom Event avec des données supplémentaires (detail)
5const escapeEvent = new CustomEvent('dinoEscape', {
6  detail: {
7    dinosaur: 'Velociraptor',
8    zone: 'B',
9    severity: 'critical'
10  }
11});
12
13// Accéder aux données de l'événement
14console.log(escapeEvent.detail.dinosaur); // "Velociraptor"
15console.log(escapeEvent.detail.severity); // "critical"
16console.log(escapeEvent.type);            // "dinoEscape"

L'objet detail est un champ spécial des Custom Events où l'on peut transmettre n'importe quelles données. C'est l'emplacement standard des données d'un CustomEvent. On peut aussi écrire sa propre classe qui hérite de Event avec des champs en plus, mais detail suffit en général.

Les options de CustomEvent

Le constructeur CustomEvent accepte un deuxième argument - un objet d'options :

1const event = new CustomEvent('systemUpdate', {
2  detail: { module: 'security', status: 'active' },  // Données de l'événement
3  bubbles: true,     // Si l'événement se propage vers le haut du DOM
4  cancelable: true,  // Si preventDefault() peut être appelé
5  composed: false    // S'il franchit les frontières du Shadow DOM
6});
  • detail - un objet avec les données qu'on veut transmettre
  • bubbles - vaut false par défaut ; mets true si l'événement doit se propager vers le haut de l'arbre du DOM (bouillonnement)
  • cancelable - vaut false par défaut ; mets true si tu veux pouvoir annuler l'événement via preventDefault()

Émettre des événements - dispatchEvent

L'objet événement en lui-même ne fait rien - on doit l'émettre (dispatch) sur un élément du DOM précis :

1// Créer un élément qui émettra des événements
2const controlPanel = document.getElementById('controlPanel');
3
4// Créer l'événement
5const fenceAlert = new CustomEvent('fenceBreak', {
6  detail: {
7    zone: 'A',
8    voltage: 0,
9    dinosaur: 'T-Rex'
10  },
11  bubbles: true
12});
13
14// Émettre l'événement sur l'élément
15controlPanel.dispatchEvent(fenceAlert);

La méthode dispatchEvent() est disponible sur chaque élément du DOM. Après son appel, l'événement est immédiatement émis et tous les gestionnaires à l'écoute sont exécutés de manière synchrone.

Écouter les Custom Events

On écoute les Custom Events exactement de la même façon que les événements ordinaires - via addEventListener :

1const controlPanel = document.getElementById('controlPanel');
2
3// Enregistrer un écouteur pour notre événement
4controlPanel.addEventListener('fenceBreak', function(event) {
5  const { zone, voltage, dinosaur } = event.detail;
6  console.log('ALARME ! Clôture de la zone ' + zone + ' endommagée !');
7  console.log('Dinosaure : ' + dinosaur);
8  console.log('Tension : ' + voltage + 'V');
9});
10
11// Un autre module peut aussi écouter le même événement
12controlPanel.addEventListener('fenceBreak', function(event) {
13  if (event.detail.dinosaur === 'T-Rex') {
14    console.log('ÉVACUEZ IMMÉDIATEMENT !');
15  }
16});
17
18// Émettre l'événement - les deux écouteurs seront appelés
19controlPanel.dispatchEvent(new CustomEvent('fenceBreak', {
20  detail: { zone: 'A', voltage: 0, dinosaur: 'T-Rex' }
21}));

Exemple pratique : système d'alarme du parc

Voyons un exemple complet de communication inter-modules :

1// Système de surveillance de Jurassic Park
2const parkSystem = document.getElementById('parkSystem');
3
4// Le module de sécurité écoute les alertes
5parkSystem.addEventListener('securityAlert', function(event) {
6  const { type, zone, message } = event.detail;
7  const alertDiv = document.createElement('div');
8  alertDiv.className = 'alert alert-' + type;
9  alertDiv.textContent = '[' + zone + '] ' + message;
10  document.getElementById('alerts').appendChild(alertDiv);
11});
12
13// Le module de surveillance des dinosaures écoute les statuts
14parkSystem.addEventListener('dinoStatus', function(event) {
15  const { name, heartRate, location } = event.detail;
16  console.log(name + ' : FC=' + heartRate + ', emplacement=' + location);
17});
18
19// Fonction qui émet une alerte de sécurité
20function emitSecurityAlert(type, zone, message) {
21  parkSystem.dispatchEvent(new CustomEvent('securityAlert', {
22    detail: { type, zone, message },
23    bubbles: true
24  }));
25}
26
27// Fonction qui émet un statut de dinosaure
28function emitDinoStatus(name, heartRate, location) {
29  parkSystem.dispatchEvent(new CustomEvent('dinoStatus', {
30    detail: { name, heartRate, location },
31    bubbles: true
32  }));
33}
34
35// Utilisation
36emitSecurityAlert('warning', 'Zone B', 'Activité des raptors élevée');
37emitSecurityAlert('critical', 'Zone A', 'Clôture endommagée !');
38emitDinoStatus('T-Rex', 85, 'Enclos principal');

Le bouillonnement dans les Custom Events

Par défaut, les Custom Events ne se propagent pas vers le haut de l'arbre du DOM. Pour activer le bouillonnement, il faut définir bubbles: true :

1const child = document.getElementById('zoneA');
2const parent = document.getElementById('parkSystem');
3
4// On enregistre l'écouteur AVANT l'envoi, car dispatchEvent() est synchrone
5parent.addEventListener('alert', (e) => {
6  console.log('Le parent a capté :', e.detail.msg);
7});
8
9// SANS bubbles (par défaut) - l'écouteur sur le parent NE captera PAS l'événement
10child.dispatchEvent(new CustomEvent('alert', {
11  detail: { msg: 'sans bubbles' }
12  // bubbles vaut false par défaut
13}));
14// (rien ne s'affiche dans la console)
15
16// AVEC bubbles: true - l'événement se propage vers le parent
17child.dispatchEvent(new CustomEvent('alert', {
18  detail: { msg: 'avec bubbles' },
19  bubbles: true  // Maintenant l'événement se propagera !
20}));
21// Le parent a capté : avec bubbles

C'est particulièrement utile quand tu veux écouter, sur un seul parent, les événements de nombreux enfants (la délégation d'événements pour les Custom Events).

Annuler les Custom Events

Si tu définis cancelable: true, tu peux annuler l'événement :

1const event = new CustomEvent('transferDino', {
2  detail: { dinosaur: 'T-Rex', from: 'A', to: 'B' },
3  cancelable: true,
4  bubbles: true
5});
6
7// Un écouteur peut annuler l'événement
8parkSystem.addEventListener('transferDino', function(e) {
9  if (e.detail.dinosaur === 'T-Rex') {
10    e.preventDefault(); // Annuler le transfert du T-Rex !
11    console.log('Transfert du T-Rex bloqué pour des raisons de sécurité !');
12  }
13});
14
15// Vérifier si l'événement a été annulé
16const wasNotCancelled = parkSystem.dispatchEvent(event);
17console.log('Transfert autorisé :', wasNotCancelled); // false (à cause de preventDefault)

La méthode dispatchEvent() retourne false si l'événement a été annulé par preventDefault(). Cela permet à l'émetteur de vérifier si l'action doit se poursuivre.

Cas d'usage des Custom Events

Les Custom Events sont parfaits dans de nombreux scénarios :

  1. Communication entre composants - quand deux modules indépendants doivent échanger des informations
  2. Système de notification - un hub central où différents modules émettent des événements
  3. Découplage du code - le module émetteur n'a pas besoin de savoir qui écoute
  4. Système de plugins - des modules externes peuvent réagir aux événements de l'application
  5. Suivi des actions utilisateur - émettre des événements d'analytics
1// Exemple : découplage avec les Custom Events
2// Le module panier ne sait rien du module de notification
3const cart = [];
4
5function addToCart(product) {
6  cart.push(product);
7
8  // Émet un événement - quiconque le souhaite peut écouter
9  document.dispatchEvent(new CustomEvent('cartUpdated', {
10    detail: { product, totalItems: cart.length },
11    bubbles: true
12  }));
13}
14
15// Le module de notification écoute indépendamment
16document.addEventListener('cartUpdated', (e) => {
17  console.log('Ajouté : ' + e.detail.product.name);
18});
19
20// Le module du badge du panier écoute aussi indépendamment
21document.addEventListener('cartUpdated', (e) => {
22  console.log('Panier : ' + e.detail.totalItems + ' articles');
23});

Récapitulatif

Les Custom Events sont un outil puissant pour construire des systèmes faiblement couplés en JavaScript :

  • new CustomEvent(name, options) - crée un événement non standard
  • element.dispatchEvent(event) - émet l'événement sur un élément
  • detail - objet avec les données transmises dans l'événement
  • bubbles: true - active la propagation de l'événement vers le haut du DOM
  • cancelable: true - permet d'annuler l'événement via preventDefault()
  • Les Custom Events s'écoutent de la même façon que les événements DOM ordinaires - via addEventListener

« À Jurassic Park », résume le Dr Grant, « chaque système doit communiquer avec les autres. Les Custom Events sont notre système radio interne - chaque module peut diffuser et chaque module peut recevoir. Sans cela, tout le parc sombrerait dans le chaos. »

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <title>Custom Events - Parc Jurassique</title>
6    <style>
7        body { font-family: monospace; background: #1a1a2e; color: #e0e0e0; padding: 20px; }
8        h1 { color: #e94560; text-align: center; }
9        h2 { color: #4ecdc4; margin-top: 25px; }
10        .section { background: rgba(255,255,255,0.05); padding: 20px; border-radius: 10px; margin: 15px 0; border-left: 3px solid #e94560; }
11        .output { background: #000; padding: 12px; border-radius: 6px; margin: 10px 0; white-space: pre-wrap; font-size: 13px; color: #0f0; max-height: 200px; overflow-y: auto; }
12        button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; margin: 5px; font-size: 14px; color: white; }
13        .btn-alert { background: #e94560; }
14        .btn-status { background: #0f3460; }
15        .btn-transfer { background: #533483; }
16        .btn-clear { background: #555; }
17        button:hover { opacity: 0.85; }
18        .zone { display: inline-block; padding: 5px 12px; margin: 3px; border-radius: 4px; background: rgba(255,255,255,0.1); }
19        .zone.active { background: rgba(233,69,96,0.3); border: 1px solid #e94560; }
20    </style>
21</head>
22<body>
23    <h1>Custom Events - Système de surveillance du parc</h1>
24
25    <div class="section">
26        <h2>1. Émettre des Custom Events</h2>
27        <p>Clique sur le bouton pour émettre un événement personnalisé avec des données :</p>
28        <button class="btn-alert" onclick="emitAlert('critical', 'A', 'Clôture endommagée !')">Alerte zone A</button>
29        <button class="btn-alert" onclick="emitAlert('warning', 'B', 'Activité des raptors élevée')">Alerte zone B</button>
30        <button class="btn-status" onclick="emitStatus('T-Rex', 85, 'Enclos principal')">Statut T-Rex</button>
31        <button class="btn-status" onclick="emitStatus('Raptor', 120, 'Zone B')">Statut Raptor</button>
32        <div class="output" id="event-log">En attente d'événements...</div>
33        <button class="btn-clear" onclick="document.getElementById('event-log').textContent = ''">Effacer le journal</button>
34    </div>
35
36    <div class="section">
37        <h2>2. Bouillonnement - propagation des événements</h2>
38        <p>Un événement avec bubbles:true se propage vers le parent :</p>
39        <div id="parkSystem" style="padding: 10px; border: 1px dashed #4ecdc4; border-radius: 8px;">
40            <span style="color: #4ecdc4;">parkSystem (parent)</span>
41            <div style="display: flex; gap: 10px; margin-top: 10px;">
42                <div class="zone" id="zoneA" onclick="emitZoneEvent('A')">Zone A (clique)</div>
43                <div class="zone" id="zoneB" onclick="emitZoneEvent('B')">Zone B (clique)</div>
44                <div class="zone" id="zoneC" onclick="emitZoneEvent('C')">Zone C (clique)</div>
45            </div>
46        </div>
47        <div class="output" id="bubble-log">Clique sur une zone...</div>
48    </div>
49
50    <div class="section">
51        <h2>3. Cancelable - annuler les événements</h2>
52        <p>Le transfert du T-Rex est bloqué par preventDefault() :</p>
53        <button class="btn-transfer" onclick="tryTransfer('T-Rex', 'A', 'B')">Transférer T-Rex (A->B)</button>
54        <button class="btn-transfer" onclick="tryTransfer('Triceratops', 'B', 'C')">Transférer Triceratops (B->C)</button>
55        <div class="output" id="transfer-log">Essaie un transfert...</div>
56    </div>
57
58    <script>
59        const eventLog = document.getElementById('event-log');
60        const bubbleLog = document.getElementById('bubble-log');
61        const transferLog = document.getElementById('transfer-log');
62        const parkSystemEl = document.getElementById('parkSystem');
63
64        function log(target, msg) {
65            const time = new Date().toLocaleTimeString();
66            target.textContent += '\n[' + time + '] ' + msg;
67            target.scrollTop = target.scrollHeight;
68        }
69
70        // === 1. Custom Events avec detail ===
71        document.addEventListener('securityAlert', function(e) {
72            const { type, zone, message } = e.detail;
73            log(eventLog, '[' + type.toUpperCase() + '] Zone ' + zone + ': ' + message);
74        });
75
76        document.addEventListener('dinoStatus', function(e) {
77            const { name, heartRate, location } = e.detail;
78            log(eventLog, '[STATUS] ' + name + ' - HR: ' + heartRate + ', loc: ' + location);
79        });
80
81        function emitAlert(type, zone, message) {
82            document.dispatchEvent(new CustomEvent('securityAlert', {
83                detail: { type, zone, message },
84                bubbles: true
85            }));
86        }
87
88        function emitStatus(name, heartRate, location) {
89            document.dispatchEvent(new CustomEvent('dinoStatus', {
90                detail: { name, heartRate, location },
91                bubbles: true
92            }));
93        }
94
95        // === 2. Bubbling demo ===
96        parkSystemEl.addEventListener('zoneAlert', function(e) {
97            log(bubbleLog, 'parkSystem A INTERCEPTÉ : Zone ' + e.detail.zone + ' (bubbling!)');
98            document.querySelectorAll('.zone').forEach(z => z.classList.remove('active'));
99            document.getElementById('zone' + e.detail.zone).classList.add('active');
100        });
101
102        function emitZoneEvent(zone) {
103            const el = document.getElementById('zone' + zone);
104            log(bubbleLog, 'Émission depuis zone' + zone + '...');
105            el.dispatchEvent(new CustomEvent('zoneAlert', {
106                detail: { zone: zone },
107                bubbles: true
108            }));
109        }
110
111        // === 3. Cancelable demo ===
112        parkSystemEl.addEventListener('transferDino', function(e) {
113            if (e.detail.dinosaur === 'T-Rex') {
114                e.preventDefault();
115                log(transferLog, 'TRANSFERT BLOQUÉ : ' + e.detail.dinosaur + ' !');
116            }
117        });
118
119        function tryTransfer(dino, from, to) {
120            const event = new CustomEvent('transferDino', {
121                detail: { dinosaur: dino, from: from, to: to },
122                cancelable: true,
123                bubbles: true
124            });
125            const allowed = parkSystemEl.dispatchEvent(event);
126            if (allowed) {
127                log(transferLog, 'OK : ' + dino + ' transféré de ' + from + ' vers ' + to);
128            } else {
129                log(transferLog, 'Transfer ' + dino + ' cancelled (preventDefault)');
130            }
131        }
132
133        console.log('Custom Events demo prêt !');
134    </script>
135</body>
136</html>

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Comment crée-t-on un événement personnalisé avec des données supplémentaires en JavaScript ?

  2. 2. Quelle méthode permet d'émettre (dispatch) un Custom Event sur un élément du DOM ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Éditeur de code

    Dans le fichier index.js, la fonction createMonitoring() construit un centre de contrôle avec un enclos à l'intérieur (l'enclos est un enfant du centre) et renvoie sendAlert(zone, level) et reportStatus(dino, status). Complète les blancs de sendAlert : ___BLANK1___ est le constructeur d'un événement qui transporte des données dans detail, ___BLANK2___ est l'option qui permet à l'événement securityAlert de remonter de l'enclos jusqu'au centre de contrôle, et ___BLANK3___ est la méthode qui envoie l'événement depuis l'enclos. Chaque alerte doit arriver dans le journal du centre sous la forme « zone: niveau », par exemple « A: élevé ».

  • Clique dans l'ordre

    Place les fragments de code pour créer un Custom Event « dinoAlert » avec un objet detail :

Articles utiles