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

Les événements personnalisés (Custom Events)

6 min de lecture
Dans cette leçon6

À Jurassic Park, différents systèmes doivent coopérer - le système de surveillance des dinosaures doit informer le centre de sécurité des menaces, et le centre de sécurité doit prévenir les équipes de secours. Les événements standards du navigateur (click, keydown, submit) gèrent les interactions de l'utilisateur, mais que faire si on a besoin de ses propres messages entre les modules de l'application ? C'est là qu'interviennent les Custom Events - des événements personnalisés, non standards.

Vois ça comme la station radio du parc. Le capteur près de la clôture n'a pas besoin de connaître les numéros de téléphone de toutes les équipes. Il diffuse un message sur sa fréquence, et chacun de ceux qui écoutent décide lui-même de ce qu'il en fait.

Créer un CustomEvent

Le constructeur CustomEvent permet de créer un événement portant n'importe quel nom et de transmettre des données supplémentaires via la propriété detail :

1// Syntaxe : new CustomEvent(eventName, options)
2const alertEvent = new CustomEvent('dinoAlert', {
3  detail: {
4    species: 'Velociraptor',
5    zone: 'B6',
6    threatLevel: 9
7  },
8  bubbles: true,      // L'événement remonte-t-il (bubbling) l'arbre DOM
9  cancelable: true     // Peut-on l'annuler avec preventDefault()
10});

La propriété detail peut contenir n'importe quelles données - objets, tableaux, chaînes, nombres. C'est la principale différence entre CustomEvent et un Event ordinaire, qui n'a pas de champ detail. Fais attention aux deux drapeaux : par défaut, bubbles et cancelable valent false, donc un événement personnalisé ne remonte pas l'arbre et ne peut pas être annulé tant que tu ne l'actives pas toi-même. Créer l'objet n'envoie encore rien.

Envoyer des événements avec dispatchEvent()

Pour « tirer » un événement, on utilise la méthode dispatchEvent() sur n'importe quel élément du DOM :

1// Envoi de l'événement sur un élément précis
2const monitorPanel = document.getElementById('monitor-panel');
3monitorPanel.dispatchEvent(alertEvent);
4
5// Envoi de l'événement sur tout le document (diffusion globale)
6document.dispatchEvent(alertEvent);

Quand l'événement est envoyé, tous les écouteurs enregistrés pour lui sont appelés de manière synchrone - dans l'ordre de leur enregistrement. C'est une différence importante par rapport aux clics de l'utilisateur : le code écrit sous dispatchEvent() ne s'exécutera que lorsque tous les écouteurs auront terminé leur travail. L'élément sur lequel tu envoies l'événement en devient la cible. Grâce à bubbles: true, le signal du panneau atteindra aussi les écouteurs placés sur ses ancêtres, jusqu'à document.

Écouter ses propres événements

L'écoute des Custom Events fonctionne exactement comme celle des événements standards - avec addEventListener() :

1// Écoute sur n'importe quel élément
2document.addEventListener('dinoAlert', function(event) {
3  const { species, zone, threatLevel } = event.detail;
4
5  console.log(`ALERTE : ${species} détecté dans la zone ${zone} !`);
6  console.log(`Niveau de menace : ${threatLevel}/10`);
7
8  if (threatLevel >= 8) {
9    activateEmergencyProtocol(zone);
10  }
11});

La déstructuration extrait de event.detail exactement les champs dont on a besoin. L'écouteur ne sait pas quel module a donné l'alerte, et il n'a pas besoin de le savoir, tandis que pour addEventListener(), un nom personnalisé ne se distingue en rien de click.

Annulation : cancelable et le résultat de dispatchEvent()

Le drapeau cancelable permet aux écouteurs de mettre leur veto à une action. La méthode dispatchEvent() renvoie false quand l'événement était annulable et qu'un des écouteurs a appelé preventDefault(), et true dans tous les autres cas.

1const gateEvent = new CustomEvent('gateOpening', {
2  detail: { gate: 'B6' },
3  cancelable: true
4});
5
6document.addEventListener('gateOpening', (e) => {
7  if (e.detail.gate === 'B6') {
8    e.preventDefault(); // des raptors près du portail - suspendre l'ouverture
9  }
10});
11
12const allowed = document.dispatchEvent(gateEvent);
13console.log(allowed ? 'Portail ouvert' : 'Ouverture bloquée');

Dans la console, tu verras « Ouverture bloquée ». Le module du portail ne connaît pas le module de sécurité, et pourtant la sécurité peut suspendre l'ouverture. Sans cancelable: true, l'appel à preventDefault() serait ignoré.

Système pratique d'alertes du parc

Réunissons tous les éléments dans un système complet de communication entre modules. Le premier module se contente de créer et d'envoyer les alertes.

1// Module 1 : Générateur d'alertes
2function createParkAlert(alertType, message, data = {}) {
3  const alertEvent = new CustomEvent('parkAlert', {
4    detail: {
5      type: alertType,
6      message: message,
7      timestamp: new Date(),
8      data: data
9    },
10    bubbles: true
11  });
12
13  document.dispatchEvent(alertEvent);
14}

La fonction createParkAlert() emballe le type, le message, l'horodatage et les données supplémentaires dans un seul detail. Le deuxième module est l'écouteur central, qui répartit les alertes selon leur type.

1// Module 2 : Système d'écoute central
2document.addEventListener('parkAlert', (e) => {
3  const { type, message, timestamp, data } = e.detail;
4
5  console.log(`${timestamp.toLocaleTimeString()} - Alerte ${type} : ${message}`);
6
7  switch(type) {
8    case 'security':
9      showSecurityAlert(message, data);
10      break;
11    case 'maintenance':
12      showMaintenanceNotification(message, data);
13      break;
14    case 'visitor':
15      showVisitorInfo(message, data);
16      break;
17    default:
18      showGeneralAlert(message);
19  }
20});

L'instruction switch dirige les alarmes de sécurité, la maintenance et les informations pour les visiteurs vers des fonctions différentes. Le troisième module détecte les évasions et utilise le générateur.

1// Module 3 : Détection des évasions de dinosaures
2function handleDinosaurEscape(dinosaurId) {
3  const dinosaur = getDinosaurById(dinosaurId);
4
5  createParkAlert('security', `ALARME ! ${dinosaur.name} s'est échappé de son enclos !`, {
6    dinosaurId: dinosaurId,
7    location: dinosaur.enclosure,
8    dangerLevel: dinosaur.dangerLevel
9  });
10
11  activateEmergencyProtocols();
12}

Le module des évasions n'appelle pas showSecurityAlert() directement. Il envoie un événement, et l'écouteur central fait le reste, donc chaque module peut être modifié ou testé séparément.

Le motif Event Bus (bus d'événements)

Les Custom Events permettent de construire un Event Bus simple - un point central de communication entre des modules indépendants de l'application :

1class ParkEventBus {
2  constructor() {
3    this.target = new EventTarget();
4  }
5
6  emit(eventName, data) {
7    this.target.dispatchEvent(
8      new CustomEvent(eventName, { detail: data })
9    );
10  }
11
12  on(eventName, callback) {
13    const handler = (e) => callback(e.detail);
14    this.target.addEventListener(eventName, handler);
15    // On renvoie une fonction de désinscription
16    return () => this.target.removeEventListener(eventName, handler);
17  }
18}

La classe s'appuie sur EventTarget, c'est-à-dire le même mécanisme que celui des éléments du DOM, mais sans aucun élément sur la page. La méthode on() renvoie une fonction de désinscription, car elle seule connaît la référence à handler. Voici comment l'utiliser :

1const bus = new ParkEventBus();
2
3// Le module de surveillance s'abonne aux alertes
4const unsubscribe = bus.on('fenceBreak', (data) => {
5  console.log(`Clôture endommagée dans la zone ${data.zone} !`);
6});
7
8// Le module des capteurs envoie une alerte
9bus.emit('fenceBreak', { zone: 'C3', voltage: 0 });
10
11// Quand le module est détruit - désinscription
12unsubscribe();

Grâce à l'Event Bus, les modules n'ont pas besoin de se connaître - ils communiquent uniquement par des événements, ce qui assure un couplage faible (loose coupling) et facilite les tests.

Mon conseil : choisis les noms de tes événements de façon à ne pas pouvoir les confondre avec les événements natifs, par exemple avec un préfixe de module, comme dinoAlert ou parkAlert, et garde un seul style dans toute l'application. Dans le bac à sable ci-dessous tourne un système d'alertes du parc construit sur les Custom Events. Nous reviendrons aux événements personnalisés avec les gestes tactiles, et nous développerons l'Event Bus dans une leçon à part.

Retiens bien : un événement personnalisé est un message diffusé par la station radio du parc, que tous ceux qui se calent sur sa fréquence entendront.

Code de cette leçon : index.html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4    <meta charset="UTF-8">
5    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6    <title>Custom Events - Système d'alerte du parc</title>
7    <style>
8        body {
9            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10            background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
11            margin: 0;
12            padding: 20px;
13            color: white;
14        }
15
16        .container {
17            max-width: 1200px;
18            margin: 0 auto;
19        }
20
21        h1 {
22            text-align: center;
23            margin-bottom: 40px;
24        }
25
26        .control-panel {
27            background: rgba(0, 0, 0, 0.8);
28            border-radius: 15px;
29            padding: 30px;
30            margin-bottom: 30px;
31        }
32
33        h2 {
34            color: #3498db;
35            margin-bottom: 20px;
36        }
37
38        .button-grid {
39            display: grid;
40            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
41            gap: 15px;
42            margin-bottom: 30px;
43        }
44
45        .alert-btn {
46            padding: 15px 20px;
47            border: none;
48            border-radius: 8px;
49            font-size: 16px;
50            font-weight: bold;
51            cursor: pointer;
52            transition: all 0.3s ease;
53            display: flex;
54            align-items: center;
55            justify-content: center;
56            gap: 10px;
57        }
58
59        .alert-btn:hover {
60            transform: translateY(-2px);
61            box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
62        }
63
64        .btn-security {
65            background: linear-gradient(45deg, #e74c3c, #c0392b);
66            color: white;
67        }
68
69        .btn-maintenance {
70            background: linear-gradient(45deg, #f39c12, #e67e22);
71            color: white;
72        }
73
74        .btn-visitor {
75            background: linear-gradient(45deg, #3498db, #2980b9);
76            color: white;
77        }
78
79        .btn-emergency {
80            background: linear-gradient(45deg, #e74c3c, #8b0000);
81            color: white;
82            animation: pulse 2s infinite;
83        }
84
85        @keyframes pulse {
86            0%, 100% { opacity: 1; }
87            50% { opacity: 0.7; }
88        }
89
90        .alerts-display {
91            display: grid;
92            grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
93            gap: 20px;
94            margin-bottom: 30px;
95        }
96
97        .alert-listener {
98            background: rgba(0, 0, 0, 0.6);
99            border-radius: 10px;
100            padding: 20px;
101            border: 2px solid #34495e;
102        }
103
104        .listener-title {
105            font-weight: bold;
106            margin-bottom: 15px;
107            color: #3498db;
108        }
109
110        .alert-box {
111            background: rgba(255, 255, 255, 0.05);
112            border-left: 4px solid;
113            border-radius: 6px;
114            padding: 15px;
115            margin: 10px 0;
116            animation: slideIn 0.5s ease;
117        }
118
119        @keyframes slideIn {
120            from {
121                opacity: 0;
122                transform: translateX(-20px);
123            }
124            to {
125                opacity: 1;
126                transform: translateX(0);
127            }
128        }
129
130        .alert-security {
131            border-color: #e74c3c;
132            background: rgba(231, 76, 60, 0.1);
133        }
134
135        .alert-maintenance {
136            border-color: #f39c12;
137            background: rgba(243, 156, 18, 0.1);
138        }
139
140        .alert-visitor {
141            border-color: #3498db;
142            background: rgba(52, 152, 219, 0.1);
143        }
144
145        .alert-header {
146            display: flex;
147            justify-content: space-between;
148            align-items: center;
149            margin-bottom: 10px;
150        }
151
152        .alert-type {
153            font-weight: bold;
154            text-transform: uppercase;
155            font-size: 12px;
156        }
157
158        .alert-time {
159            color: #95a5a6;
160            font-size: 11px;
161        }
162
163        .alert-message {
164            margin-bottom: 10px;
165        }
166
167        .alert-details {
168            background: rgba(0, 0, 0, 0.3);
169            border-radius: 4px;
170            padding: 8px;
171            font-size: 12px;
172            font-family: 'Courier New', monospace;
173            color: #bdc3c7;
174        }
175
176        .statistics {
177            background: rgba(0, 0, 0, 0.8);
178            border-radius: 15px;
179            padding: 30px;
180        }
181
182        .stats-grid {
183            display: grid;
184            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
185            gap: 20px;
186        }
187
188        .stat-box {
189            background: rgba(255, 255, 255, 0.05);
190            border-radius: 8px;
191            padding: 20px;
192            text-align: center;
193        }
194
195        .stat-number {
196            font-size: 36px;
197            font-weight: bold;
198            color: #3498db;
199        }
200
201        .stat-label {
202            color: #bdc3c7;
203            font-size: 13px;
204            margin-top: 5px;
205        }
206
207        .clear-btn {
208            padding: 10px 20px;
209            background: #7f8c8d;
210            color: white;
211            border: none;
212            border-radius: 6px;
213            cursor: pointer;
214            font-weight: bold;
215            margin-top: 20px;
216        }
217
218        .clear-btn:hover {
219            background: #95a5a6;
220        }
221    </style>
222</head>
223<body>
224    <div class="container">
225        <h1>Système Custom Events - Parc Jurassique</h1>
226
227        <div class="control-panel">
228            <h2>Panneau de contrôle - Envoyer une alerte</h2>
229            <div class="button-grid">
230                <button class="alert-btn btn-security" onclick="sendSecurityAlert()">
231                    Security Alert
232                </button>
233                <button class="alert-btn btn-maintenance" onclick="sendMaintenanceAlert()">
234                    Maintenance Report
235                </button>
236                <button class="alert-btn btn-visitor" onclick="sendVisitorInfo()">
237                    Visitor Information
238                </button>
239                <button class="alert-btn btn-emergency" onclick="sendEmergencyAlert()">
240                    ALARME D'URGENCE
241                </button>
242            </div>
243        </div>
244
245        <div class="alerts-display">
246            <div class="alert-listener">
247                <div class="listener-title">Security Center</div>
248                <div id="security-alerts"></div>
249            </div>
250
251            <div class="alert-listener">
252                <div class="listener-title">Maintenance Department</div>
253                <div id="maintenance-alerts"></div>
254            </div>
255
256            <div class="alert-listener">
257                <div class="listener-title">Visitor Services</div>
258                <div id="visitor-alerts"></div>
259            </div>
260
261            <div class="alert-listener">
262                <div class="listener-title">Toutes les alertes</div>
263                <div id="all-alerts"></div>
264            </div>
265        </div>
266
267        <div class="statistics">
268            <h2>Statistiques d'alertes</h2>
269            <div class="stats-grid">
270                <div class="stat-box">
271                    <div class="stat-number" id="stat-security">0</div>
272                    <div class="stat-label">Security alerts</div>
273                </div>
274                <div class="stat-box">
275                    <div class="stat-number" id="stat-maintenance">0</div>
276                    <div class="stat-label">Maintenance reports</div>
277                </div>
278                <div class="stat-box">
279                    <div class="stat-number" id="stat-visitor">0</div>
280                    <div class="stat-label">Visitor information</div>
281                </div>
282                <div class="stat-box">
283                    <div class="stat-number" id="stat-total">0</div>
284                    <div class="stat-label">Toutes les alertes</div>
285                </div>
286            </div>
287            <button class="clear-btn" onclick="clearAllAlerts()">Effacer toutes les alertes</button>
288        </div>
289    </div>
290
291    <script>
292        console.log('Système Custom Events - Initialisation...');
293
294        // Statistiques
295        let stats = {
296            security: 0,
297            maintenance: 0,
298            visitor: 0,
299            total: 0
300        };
301
302        // Fonction pour créer un custom event
303        function createParkAlert(alertType, message, data = {}) {
304            const alertEvent = new CustomEvent('parkAlert', {
305                detail: {
306                    type: alertType,
307                    message: message,
308                    timestamp: new Date(),
309                    data: data
310                },
311                bubbles: true,
312                cancelable: true
313            });
314
315            document.dispatchEvent(alertEvent);
316            console.log(`Custom Event dispatched: parkAlert [${alertType}]`, data);
317        }
318
319        // Function to display alert
320        function displayAlert(container, type, message, timestamp, data) {
321            const alertBox = document.createElement('div');
322            alertBox.className = `alert-box alert-${type}`;
323
324            const typeLabels = {
325                security: 'SECURITY',
326                maintenance: 'MAINTENANCE',
327                visitor: 'VISITORS'
328            };
329
330            alertBox.innerHTML = `
331                <div class="alert-header">
332                    <span class="alert-type">${typeLabels[type]}</span>
333                    <span class="alert-time">${timestamp.toLocaleTimeString()}</span>
334                </div>
335                <div class="alert-message">${message}</div>
336                <div class="alert-details">
337                    Details: ${JSON.stringify(data, null, 2)}
338                </div>
339            `;
340
341            container.insertBefore(alertBox, container.firstChild);
342
343            // Limit the number of displayed alerts
344            while (container.children.length > 3) {
345                container.removeChild(container.lastChild);
346            }
347        }
348
349        // Listeners for different departments
350
351        // 1. Security Center - alertes security uniquement
352        document.addEventListener('parkAlert', (e) => {
353            if (e.detail.type === 'security') {
354                const container = document.getElementById('security-alerts');
355                displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
356                console.log('Security Center received alert:', e.detail);
357            }
358        });
359
360        // 2. Maintenance Department - maintenance uniquement
361        document.addEventListener('parkAlert', (e) => {
362            if (e.detail.type === 'maintenance') {
363                const container = document.getElementById('maintenance-alerts');
364                displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
365                console.log('Maintenance Department received report:', e.detail);
366            }
367        });
368
369        // 3. Visitor Services - visitor uniquement
370        document.addEventListener('parkAlert', (e) => {
371            if (e.detail.type === 'visitor') {
372                const container = document.getElementById('visitor-alerts');
373                displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
374                console.log('Visitor Services received information:', e.detail);
375            }
376        });
377
378        // 4. Toutes les alertes - tous types
379        document.addEventListener('parkAlert', (e) => {
380            const container = document.getElementById('all-alerts');
381            displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
382
383            // Mise à jour des statistiques
384            stats[e.detail.type]++;
385            stats.total++;
386
387            document.getElementById('stat-security').textContent = stats.security;
388            document.getElementById('stat-maintenance').textContent = stats.maintenance;
389            document.getElementById('stat-visitor').textContent = stats.visitor;
390            document.getElementById('stat-total').textContent = stats.total;
391        });
392
393        // Alert sending functions
394
395        function sendSecurityAlert() {
396            const dinosaurs = ['T-Rex', 'Velociraptor', 'Dilophosaurus', 'Pteranodon'];
397            const locations = ['Sector A', 'Sector B', 'Laboratory', 'Main Gate'];
398            const randomDino = dinosaurs[Math.floor(Math.random() * dinosaurs.length)];
399            const randomLocation = locations[Math.floor(Math.random() * locations.length)];
400
401            createParkAlert('security', `ATTENTION ! ${randomDino} détecté hors de la zone désignée !`, {
402                dinosaur: randomDino,
403                location: randomLocation,
404                dangerLevel: Math.floor(Math.random() * 10) + 1,
405                status: 'ACTIF'
406            });
407        }
408
409        function sendMaintenanceAlert() {
410            const systems = ['Fence System', 'Power Generator', 'Climate System', 'Security Doors'];
411            const randomSystem = systems[Math.floor(Math.random() * systems.length)];
412
413            createParkAlert('maintenance', `Malfunction report: ${randomSystem} requires inspection`, {
414                system: randomSystem,
415                priority: ['High', 'Medium', 'Low'][Math.floor(Math.random() * 3)],
416                estimatedTime: `${Math.floor(Math.random() * 4) + 1}h`
417            });
418        }
419
420        function sendVisitorInfo() {
421            const messages = [
422                'Triceratops feeding show is starting',
423                'Visite VIP dans 15 minutes',
424                'New exhibit "Age of Dinosaurs" now open',
425                'Please maintain a safe distance'
426            ];
427            const randomMessage = messages[Math.floor(Math.random() * messages.length)];
428
429            createParkAlert('visitor', randomMessage, {
430                category: 'Information',
431                targetGroup: 'All visitors',
432                validUntil: new Date(Date.now() + 3600000).toLocaleTimeString()
433            });
434        }
435
436        function sendEmergencyAlert() {
437            createParkAlert('security', "ALARME D'URGENCE ! Évacuation immédiate !", {
438                emergency: true,
439                evacuationRoute: 'Main Gate',
440                status: 'CRITIQUE',
441                actionRequired: 'Immediate evacuation of all visitors and staff'
442            });
443
444            // Additional alert for all departments
445            setTimeout(() => {
446                createParkAlert('maintenance', 'All security systems in emergency mode', {
447                    emergency: true
448                });
449            }, 500);
450
451            setTimeout(() => {
452                createParkAlert('visitor', 'Please immediately proceed to evacuation points', {
453                    emergency: true
454                });
455            }, 1000);
456        }
457
458        function clearAllAlerts() {
459            document.getElementById('security-alerts').innerHTML = '';
460            document.getElementById('maintenance-alerts').innerHTML = '';
461            document.getElementById('visitor-alerts').innerHTML = '';
462            document.getElementById('all-alerts').innerHTML = '';
463
464            stats = { security: 0, maintenance: 0, visitor: 0, total: 0 };
465            document.getElementById('stat-security').textContent = '0';
466            document.getElementById('stat-maintenance').textContent = '0';
467            document.getElementById('stat-visitor').textContent = '0';
468            document.getElementById('stat-total').textContent = '0';
469
470            console.log('Toutes les alertes effacées');
471        }
472
473        console.log('Système Custom Events prêt à fonctionner');
474        console.log('Registered 4 listeners for event "parkAlert"');
475        console.log('Parc Jurassique - Système de Communication ONLINE');
476    </script>
477</body>
478</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. À quoi sert la propriété detail d'un CustomEvent ?

  2. 2. Quelle méthode utilise-t-on pour « déclencher » un événement personnalisé sur un élément du DOM ?

Tâches pratiques dans le jeu

  • Tri horizontal

    Ordonne la création d'un événement personnalisé (une ligne, de gauche à droite) :

Articles utiles