JavaScript and TypeScript course ยท Module 8: Events and the DOM
Custom Events
In this lesson6
In Jurassic Park, different systems must cooperate - the dinosaur monitoring system must inform the security center about threats, and the security center must notify rescue teams. Standard browser events (click, keydown, submit) handle user interactions, but what if we need custom messages between application modules? This is where Custom Events come to the rescue - custom, non-standard events.
Think of it as the park's radio station. A sensor by the fence doesn't need to know the phone numbers of all the teams. It broadcasts a message on its frequency, and everyone who listens decides for themselves what to do with it.
Creating a CustomEvent
The CustomEvent constructor allows you to create an event with any name and pass additional data through the detail property:
// Syntax: new CustomEvent(eventName, options)
const alertEvent = new CustomEvent('dinoAlert', {
detail: {
species: 'Velociraptor',
zone: 'B6',
threatLevel: 9
},
bubbles: true, // Whether the event bubbles up the DOM tree
cancelable: true // Whether it can be canceled via preventDefault()
});The detail property can contain any data - objects, arrays, strings, numbers. This is the main difference between CustomEvent and a regular Event, which doesn't have a detail field. Pay attention to both flags: by default bubbles and cancelable are false, so a custom event doesn't travel up the tree and can't be canceled until you switch that on yourself. Creating the object alone doesn't send anything yet.
Dispatching Events with dispatchEvent()
To "fire" an event, we use the dispatchEvent() method on any DOM element:
// Dispatching an event on a specific element
const monitorPanel = document.getElementById('monitor-panel');
monitorPanel.dispatchEvent(alertEvent);
// Dispatching an event on the entire document (global broadcast)
document.dispatchEvent(alertEvent);When an event is dispatched, all registered listeners for it are called synchronously - in the order they were registered. This is an important difference from user clicks: the code written below dispatchEvent() runs only once all the listeners have finished their work. The element you dispatch the event on becomes its target. Thanks to bubbles: true, the signal from the panel also reaches listeners on its ancestors, all the way up to document.
Listening for Custom Events
Listening for Custom Events works identically to standard events - using addEventListener():
// Listening on any element
document.addEventListener('dinoAlert', function(event) {
const { species, zone, threatLevel } = event.detail;
console.log(`ALERT: ${species} detected in zone ${zone}!`);
console.log(`Threat level: ${threatLevel}/10`);
if (threatLevel >= 8) {
activateEmergencyProtocol(zone);
}
});Destructuring pulls exactly the fields we need out of event.detail. The listener doesn't know which module raised the alarm, and it doesn't need to, while for addEventListener() a custom name is no different from click.
Canceling: cancelable and the Result of dispatchEvent()
The cancelable flag lets listeners veto an action. The dispatchEvent() method returns false when the event was cancelable and one of the listeners called preventDefault(), and true in every other case.
const gateEvent = new CustomEvent('gateOpening', {
detail: { gate: 'B6' },
cancelable: true
});
document.addEventListener('gateOpening', (e) => {
if (e.detail.gate === 'B6') {
e.preventDefault(); // raptors by the gate - hold the opening
}
});
const allowed = document.dispatchEvent(gateEvent);
console.log(allowed ? 'Gate opened' : 'Opening blocked');The console will show "Opening blocked". The gate module doesn't know the security module, and yet security can hold the opening. Without cancelable: true, the preventDefault() call would be ignored.
Practical Park Alert System
Let's combine all elements into a complete inter-module communication system. The first module only creates and dispatches alerts.
// Module 1: Alert generator
function createParkAlert(alertType, message, data = {}) {
const alertEvent = new CustomEvent('parkAlert', {
detail: {
type: alertType,
message: message,
timestamp: new Date(),
data: data
},
bubbles: true
});
document.dispatchEvent(alertEvent);
}The createParkAlert() function packs the type, message, timestamp, and extra data into a single detail. The second module is the central listener that routes alerts by type.
// Module 2: Central listening system
document.addEventListener('parkAlert', (e) => {
const { type, message, timestamp, data } = e.detail;
console.log(`${timestamp.toLocaleTimeString()} - Alert ${type}: ${message}`);
switch(type) {
case 'security':
showSecurityAlert(message, data);
break;
case 'maintenance':
showMaintenanceNotification(message, data);
break;
case 'visitor':
showVisitorInfo(message, data);
break;
default:
showGeneralAlert(message);
}
});The switch statement sends security alarms, maintenance, and visitor information to different functions. The third module detects escapes and uses the generator.
// Module 3: Dinosaur escape detection
function handleDinosaurEscape(dinosaurId) {
const dinosaur = getDinosaurById(dinosaurId);
createParkAlert('security', `ALARM! ${dinosaur.name} escaped from the enclosure!`, {
dinosaurId: dinosaurId,
location: dinosaur.enclosure,
dangerLevel: dinosaur.dangerLevel
});
activateEmergencyProtocols();
}The escape module doesn't call showSecurityAlert() directly. It dispatches an event and the central listener does the rest, so each module can be changed or tested on its own.
Event Bus Pattern
Custom Events allow you to build a simple Event Bus - a central communication point between independent application modules:
class ParkEventBus {
constructor() {
this.target = new EventTarget();
}
emit(eventName, data) {
this.target.dispatchEvent(
new CustomEvent(eventName, { detail: data })
);
}
on(eventName, callback) {
const handler = (e) => callback(e.detail);
this.target.addEventListener(eventName, handler);
// Return a function to unsubscribe
return () => this.target.removeEventListener(eventName, handler);
}
}The class is built on EventTarget, the same mechanism that DOM elements use, just without any element on the page. The on() method returns an unsubscribe function, because only it knows the reference to handler. Here is how it's used:
const bus = new ParkEventBus();
// Monitoring module subscribes to alerts
const unsubscribe = bus.on('fenceBreak', (data) => {
console.log(`Fence in zone ${data.zone} damaged!`);
});
// Sensor module sends alert
bus.emit('fenceBreak', { zone: 'C3', voltage: 0 });
// When the module is destroyed - unsubscribe
unsubscribe();Thanks to Event Bus, modules don't need to know about each other - they communicate exclusively through events, which ensures loose coupling and easier testing.
My advice: choose names for your own events so that they can't be confused with native ones, for example with a module prefix, like dinoAlert or parkAlert, and stick to one style throughout the application. In the sandbox below, a park alert system built on Custom Events is running. We'll come back to custom events with touch gestures, and we'll expand the Event Bus in a separate lesson.
Remember: a custom event is a message broadcast by the park's radio station, heard by everyone tuned in to its frequency.
Code for this lesson: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom Events - Park Alert System</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
margin: 0;
padding: 20px;
color: white;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
text-align: center;
margin-bottom: 40px;
}
.control-panel {
background: rgba(0, 0, 0, 0.8);
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
}
h2 {
color: #3498db;
margin-bottom: 20px;
}
.button-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.alert-btn {
padding: 15px 20px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.alert-btn:hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.btn-security {
background: linear-gradient(45deg, #e74c3c, #c0392b);
color: white;
}
.btn-maintenance {
background: linear-gradient(45deg, #f39c12, #e67e22);
color: white;
}
.btn-visitor {
background: linear-gradient(45deg, #3498db, #2980b9);
color: white;
}
.btn-emergency {
background: linear-gradient(45deg, #e74c3c, #8b0000);
color: white;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.alerts-display {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.alert-listener {
background: rgba(0, 0, 0, 0.6);
border-radius: 10px;
padding: 20px;
border: 2px solid #34495e;
}
.listener-title {
font-weight: bold;
margin-bottom: 15px;
color: #3498db;
}
.alert-box {
background: rgba(255, 255, 255, 0.05);
border-left: 4px solid;
border-radius: 6px;
padding: 15px;
margin: 10px 0;
animation: slideIn 0.5s ease;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.alert-security {
border-color: #e74c3c;
background: rgba(231, 76, 60, 0.1);
}
.alert-maintenance {
border-color: #f39c12;
background: rgba(243, 156, 18, 0.1);
}
.alert-visitor {
border-color: #3498db;
background: rgba(52, 152, 219, 0.1);
}
.alert-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.alert-type {
font-weight: bold;
text-transform: uppercase;
font-size: 12px;
}
.alert-time {
color: #95a5a6;
font-size: 11px;
}
.alert-message {
margin-bottom: 10px;
}
.alert-details {
background: rgba(0, 0, 0, 0.3);
border-radius: 4px;
padding: 8px;
font-size: 12px;
font-family: 'Courier New', monospace;
color: #bdc3c7;
}
.statistics {
background: rgba(0, 0, 0, 0.8);
border-radius: 15px;
padding: 30px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 20px;
}
.stat-box {
background: rgba(255, 255, 255, 0.05);
border-radius: 8px;
padding: 20px;
text-align: center;
}
.stat-number {
font-size: 36px;
font-weight: bold;
color: #3498db;
}
.stat-label {
color: #bdc3c7;
font-size: 13px;
margin-top: 5px;
}
.clear-btn {
padding: 10px 20px;
background: #7f8c8d;
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-weight: bold;
margin-top: 20px;
}
.clear-btn:hover {
background: #95a5a6;
}
</style>
</head>
<body>
<div class="container">
<h1>Custom Events System - Jurassic Park</h1>
<div class="control-panel">
<h2>Control Panel - Send Alert</h2>
<div class="button-grid">
<button class="alert-btn btn-security" onclick="sendSecurityAlert()">
Security Alert
</button>
<button class="alert-btn btn-maintenance" onclick="sendMaintenanceAlert()">
Maintenance Report
</button>
<button class="alert-btn btn-visitor" onclick="sendVisitorInfo()">
Visitor Information
</button>
<button class="alert-btn btn-emergency" onclick="sendEmergencyAlert()">
EMERGENCY ALARM
</button>
</div>
</div>
<div class="alerts-display">
<div class="alert-listener">
<div class="listener-title">Security Center</div>
<div id="security-alerts"></div>
</div>
<div class="alert-listener">
<div class="listener-title">Maintenance Department</div>
<div id="maintenance-alerts"></div>
</div>
<div class="alert-listener">
<div class="listener-title">Visitor Services</div>
<div id="visitor-alerts"></div>
</div>
<div class="alert-listener">
<div class="listener-title">All Alerts</div>
<div id="all-alerts"></div>
</div>
</div>
<div class="statistics">
<h2>Alert Statistics</h2>
<div class="stats-grid">
<div class="stat-box">
<div class="stat-number" id="stat-security">0</div>
<div class="stat-label">Security alerts</div>
</div>
<div class="stat-box">
<div class="stat-number" id="stat-maintenance">0</div>
<div class="stat-label">Maintenance reports</div>
</div>
<div class="stat-box">
<div class="stat-number" id="stat-visitor">0</div>
<div class="stat-label">Visitor information</div>
</div>
<div class="stat-box">
<div class="stat-number" id="stat-total">0</div>
<div class="stat-label">All alerts</div>
</div>
</div>
<button class="clear-btn" onclick="clearAllAlerts()">Clear all alerts</button>
</div>
</div>
<script>
console.log('Custom Events system - Initializing...');
// Statistics
let stats = {
security: 0,
maintenance: 0,
visitor: 0,
total: 0
};
// Function that creates a custom event
function createParkAlert(alertType, message, data = {}) {
const alertEvent = new CustomEvent('parkAlert', {
detail: {
type: alertType,
message: message,
timestamp: new Date(),
data: data
},
bubbles: true,
cancelable: true
});
document.dispatchEvent(alertEvent);
console.log(`Custom Event dispatched: parkAlert [${alertType}]`, data);
}
// Function to display alert
function displayAlert(container, type, message, timestamp, data) {
const alertBox = document.createElement('div');
alertBox.className = `alert-box alert-${type}`;
const typeLabels = {
security: 'SECURITY',
maintenance: 'MAINTENANCE',
visitor: 'VISITORS'
};
alertBox.innerHTML = `
<div class="alert-header">
<span class="alert-type">${typeLabels[type]}</span>
<span class="alert-time">${timestamp.toLocaleTimeString()}</span>
</div>
<div class="alert-message">${message}</div>
<div class="alert-details">
Details: ${JSON.stringify(data, null, 2)}
</div>
`;
container.insertBefore(alertBox, container.firstChild);
// Limit the number of displayed alerts
while (container.children.length > 3) {
container.removeChild(container.lastChild);
}
}
// Listeners for different departments
// 1. Security Center - security alerts only
document.addEventListener('parkAlert', (e) => {
if (e.detail.type === 'security') {
const container = document.getElementById('security-alerts');
displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
console.log('Security Center received alert:', e.detail);
}
});
// 2. Maintenance Department - maintenance only
document.addEventListener('parkAlert', (e) => {
if (e.detail.type === 'maintenance') {
const container = document.getElementById('maintenance-alerts');
displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
console.log('Maintenance Department received report:', e.detail);
}
});
// 3. Visitor Services - visitor info only
document.addEventListener('parkAlert', (e) => {
if (e.detail.type === 'visitor') {
const container = document.getElementById('visitor-alerts');
displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
console.log('Visitor Services received information:', e.detail);
}
});
// 4. All Alerts - every type
document.addEventListener('parkAlert', (e) => {
const container = document.getElementById('all-alerts');
displayAlert(container, e.detail.type, e.detail.message, e.detail.timestamp, e.detail.data);
// Updating the statistics
stats[e.detail.type]++;
stats.total++;
document.getElementById('stat-security').textContent = stats.security;
document.getElementById('stat-maintenance').textContent = stats.maintenance;
document.getElementById('stat-visitor').textContent = stats.visitor;
document.getElementById('stat-total').textContent = stats.total;
});
// Alert sending functions
function sendSecurityAlert() {
const dinosaurs = ['T-Rex', 'Velociraptor', 'Dilophosaurus', 'Pteranodon'];
const locations = ['Sector A', 'Sector B', 'Laboratory', 'Main Gate'];
const randomDino = dinosaurs[Math.floor(Math.random() * dinosaurs.length)];
const randomLocation = locations[Math.floor(Math.random() * locations.length)];
createParkAlert('security', `WARNING! ${randomDino} detected outside its designated area!`, {
dinosaur: randomDino,
location: randomLocation,
dangerLevel: Math.floor(Math.random() * 10) + 1,
status: 'ACTIVE'
});
}
function sendMaintenanceAlert() {
const systems = ['Fence System', 'Power Generator', 'Climate System', 'Security Doors'];
const randomSystem = systems[Math.floor(Math.random() * systems.length)];
createParkAlert('maintenance', `Malfunction report: ${randomSystem} requires inspection`, {
system: randomSystem,
priority: ['High', 'Medium', 'Low'][Math.floor(Math.random() * 3)],
estimatedTime: `${Math.floor(Math.random() * 4) + 1}h`
});
}
function sendVisitorInfo() {
const messages = [
'Triceratops feeding show is starting',
'VIP tour in 15 minutes',
'New exhibit "Age of Dinosaurs" now open',
'Please maintain a safe distance'
];
const randomMessage = messages[Math.floor(Math.random() * messages.length)];
createParkAlert('visitor', randomMessage, {
category: 'Information',
targetGroup: 'All visitors',
validUntil: new Date(Date.now() + 3600000).toLocaleTimeString()
});
}
function sendEmergencyAlert() {
createParkAlert('security', 'EMERGENCY ALARM! Evacuate immediately!', {
emergency: true,
evacuationRoute: 'Main Gate',
status: 'CRITICAL',
actionRequired: 'Immediate evacuation of all visitors and staff'
});
// Additional alert for all departments
setTimeout(() => {
createParkAlert('maintenance', 'All security systems in emergency mode', {
emergency: true
});
}, 500);
setTimeout(() => {
createParkAlert('visitor', 'Please immediately proceed to evacuation points', {
emergency: true
});
}, 1000);
}
function clearAllAlerts() {
document.getElementById('security-alerts').innerHTML = '';
document.getElementById('maintenance-alerts').innerHTML = '';
document.getElementById('visitor-alerts').innerHTML = '';
document.getElementById('all-alerts').innerHTML = '';
stats = { security: 0, maintenance: 0, visitor: 0, total: 0 };
document.getElementById('stat-security').textContent = '0';
document.getElementById('stat-maintenance').textContent = '0';
document.getElementById('stat-visitor').textContent = '0';
document.getElementById('stat-total').textContent = '0';
console.log('All alerts cleared');
}
console.log('Custom Events system ready');
console.log('Registered 4 listeners for event "parkAlert"');
console.log('Jurassic Park - Communication System ONLINE');
</script>
</body>
</html>Docs entries used in this lesson
Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What is the
detailproperty of aCustomEventused for?2. Which method do we use to "fire" a custom event on a DOM element?
Hands-on tasks in the game
- Horizontal ordering
Arrange the creation of a custom event (one line, left to right):