JavaScript and TypeScript course Β· Module 8: Events and the DOM
Event Bus and Application Architecture
Implementing a communication system between components using an event bus.
Imagine a module that detects dinosaur escapes. When a raptor breaks through the fence, you need to turn on the siren, mark the zone on the map, lock the gates, and send a message to security. If the escape module calls each of those functions directly, every new system means digging through its code, and after a year nobody knows what depends on what. An Event Bus works like the park's radio center: modules broadcast messages on a shared bus, and those interested listen to them on their own. Nobody needs to know anyone's address.
Event Bus Implementation
The class stores listeners in the events object. The key is the event name, and the value is an array of functions waiting for it.
class EventBus {
constructor() {
this.events = {};
}
// Register a listener
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
// Return a function to remove the listener
return () => this.off(eventName, callback);
}The on() method creates the array on the first registration, adds the function to it, and returns a ready unsubscribe function. Thanks to that, a module doesn't have to store a reference to its callback just to remove it later. The eventBus.on('alert', callback) call reads like a sentence: listen for alert and then call the callback. The next two methods remove listeners.
// Remove a listener
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
// One-time listener
once(eventName, callback) {
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}The off() method doesn't modify the old array, it creates a new one with filter(), so removing a listener during an emit doesn't break the loop in progress. The once() method wraps the callback in onceCallback, which first calls the original and then unsubscribes itself. So the listener runs exactly once and disappears automatically. This version has one limitation: we stored onceCallback, not the original callback, so calling off() with the original won't remove it. Time to broadcast messages.
// Emit an event
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in event listener for ${eventName}:`, error);
}
});
}
}The emit() method passes all the arguments to every listener in the order of registration. The try...catch block around each call is an important architectural decision: an error in one module goes to the console but doesn't stop notifications for the others. The siren will sound even if the map module happens to be broken. Finally, bulk cleanup.
// Remove all listeners for an event
removeAllListeners(eventName) {
if (eventName) {
delete this.events[eventName];
} else {
this.events = {};
}
}
}The removeAllListeners() method with a name removes one channel, and without an argument it clears the whole bus. It comes in handy when shutting down the application and in tests, when every test should start with a clean radio center.
Using It in Application Modules
In the application we create one shared instance that all modules import. Module A subscribes to the escape message.
// Global event bus instance
const eventBus = new EventBus();
// Usage in different application modules
// Module A
eventBus.on('dinosaurEscape', (dinosaurData) => {
showEmergencyAlert(dinosaurData);
});Module A will show the alarm as soon as someone broadcasts dinosaurEscape, although it doesn't even know which module will do it. Now the sender and the second listener.
// Module B
function handleDinosaurEscape(dinosaur) {
eventBus.emit('dinosaurEscape', {
id: dinosaur.id,
name: dinosaur.name,
location: dinosaur.enclosure,
dangerLevel: dinosaur.dangerLevel
});
}
// Module C
eventBus.on('dinosaurEscape', (dinosaurData) => {
if (dinosaurData.dangerLevel > 8) {
activateEmergencyProtocols();
}
});Module B only broadcasts a message with the dinosaur's data. It doesn't know that modules A and C exist, and module C decides for itself that it reacts only to a danger level above 8. That's loose coupling: adding a module D is one new on(), without any change to module B.
When to Reach for an Event Bus
In the lesson on custom events, we built a bus on EventTarget and CustomEvent, with the data in detail. This version is built on a plain JavaScript object, so it also works outside the browser. In fact, Node.js has a built-in EventEmitter class with the same on, off, once, and emit methods.
My advice: use the bus for notifications that interest many independent modules, but don't turn every function call into a message. With too many events it's hard to answer the question of who sent what and why. Keep event names in constants, because a typo in 'dinosaurEscape' won't raise an error, it will just turn the alarm into radio silence. In the sandbox below you'll see a bus connecting several park modules. In the next lesson you'll learn to test event handlers.
Remember: an Event Bus is the park's radio center, where the sender doesn't need to know the listeners and the listeners don't need to know the sender.
Code for this lesson: index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Event Bus Pattern</title>
<style>
body { font-family: Arial, sans-serif; background: linear-gradient(135deg, #2c3e50, #34495e); color: white; padding: 20px; }
.module { background: rgba(0,0,0,0.7); padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 4px solid #3498db; }
button { padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 6px; cursor: pointer; margin: 5px; }
.log { background: #000; padding: 15px; border-radius: 6px; font-family: monospace; font-size: 12px; max-height: 150px; overflow-y: auto; }
.log-entry { padding: 3px 0; border-bottom: 1px solid #333; }
</style>
</head>
<body>
<h1>Event Bus Architecture</h1>
<div class="module">
<h3>Module A: Security System</h3>
<button onclick="triggerSecurityAlert()">Trigger Security Alert</button>
<div class="log" id="security-log"></div>
</div>
<div class="module">
<h3>Module B: Visitor Management</h3>
<button onclick="addVisitor()">Add Visitor</button>
<div class="log" id="visitor-log"></div>
</div>
<div class="module">
<h3>Module C: Analytics</h3>
<div class="log" id="analytics-log"></div>
</div>
<script>
console.log('Event Bus System initializing...');
// EVENT BUS IMPLEMENTATION
class EventBus {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
return () => this.off(eventName, callback);
}
off(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(cb => cb !== callback);
}
}
emit(eventName, ...args) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => {
try {
callback(...args);
} catch (error) {
console.error(`Error in ${eventName}:`, error);
}
});
}
}
once(eventName, callback) {
const onceCallback = (...args) => {
callback(...args);
this.off(eventName, onceCallback);
};
this.on(eventName, onceCallback);
}
}
// Global Event Bus instance
const eventBus = new EventBus();
// Helper functions
function log(containerId, message, color = '#95a5a6') {
const container = document.getElementById(containerId);
const entry = document.createElement('div');
entry.className = 'log-entry';
entry.style.color = color;
entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
container.insertBefore(entry, container.firstChild);
if (container.children.length > 10) {
container.removeChild(container.lastChild);
}
}
// MODULE A: Security System
eventBus.on('security:alert', (data) => {
log('security-log', `ALERT: ${data.message}`, '#e74c3c');
log('security-log', `Location: ${data.location}`, '#e74c3c');
console.log('Security module received alert:', data);
});
eventBus.on('visitor:entered', (data) => {
log('security-log', `Visitor ${data.name} cleared security`, '#27ae60');
});
// MODULE B: Visitor Management
eventBus.on('visitor:entered', (data) => {
log('visitor-log', `New visitor: ${data.name}`, '#3498db');
log('visitor-log', `Ticket: ${data.ticketType}`, '#3498db');
});
eventBus.on('security:alert', (data) => {
log('visitor-log', `Evacuation notice sent`, '#f39c12');
});
// MODULE C: Analytics
let visitorCount = 0;
let alertCount = 0;
eventBus.on('visitor:entered', (data) => {
visitorCount++;
log('analytics-log', `Total visitors: ${visitorCount}`, '#9b59b6');
});
eventBus.on('security:alert', (data) => {
alertCount++;
log('analytics-log', `Total alerts: ${alertCount}`, '#9b59b6');
});
// Trigger functions
function triggerSecurityAlert() {
eventBus.emit('security:alert', {
message: 'T-Rex escaped from enclosure!',
location: 'Sector 7',
severity: 'HIGH',
timestamp: new Date()
});
}
function addVisitor() {
const names = ['John', 'Sarah', 'Mike', 'Emma'];
const tickets = ['Standard', 'VIP', 'Family'];
eventBus.emit('visitor:entered', {
name: names[Math.floor(Math.random() * names.length)],
ticketType: tickets[Math.floor(Math.random() * tickets.length)],
timestamp: new Date()
});
}
console.log('Event Bus ready');
console.log('3 modules connected via Event Bus');
log('security-log', 'Security System online', '#27ae60');
log('visitor-log', 'Visitor Management online', '#27ae60');
log('analytics-log', 'Analytics System online', '#27ae60');
</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 main advantage of the Event Bus pattern in application architecture?
2. What does the once() method do in an Event Bus implementation?
Hands-on tasks in the game
- Code editor
The index.js file has an EventBus class with on(), off(), emit() and once() methods, as in the Event Bus lesson. Fill in the blanks: ___BLANK1___ is the array method that keeps every listener in off() except the removed one, and ___BLANK2___ is the EventBus method the one-time listener from once() uses to unsubscribe before calling the callback. emit() should call the listeners in the order they were added with the given arguments, off() should remove only the given listener, and a listener from once() should run only on the first emit().
- Horizontal ordering
Arrange the Event Bus method call for listening to the 'alert' event: