Cours JavaScript et TypeScript · Module 8: Événements et DOM
Les événements personnalisés (Custom Events)
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. À quoi sert la propriété
detaild'unCustomEvent?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) :