In Jurassic Park's control center, every panel on the screen is a separate component - a dinosaur card, an alert window, a report form. Each responds to operator actions: clicks, keys, mouse hover. Building such interactive components is one of the most common use cases for event listeners in real applications.
In this lesson, you'll learn to create self-contained, reusable UI components that manage their own events - from modal windows to panels with dynamic content.
A modal is a window displayed on top of the page, blocking interaction with the rest of the interface. In our park, we'll use it to display dinosaur details after clicking its card.
1function createDinosaurModal(dinosaurData) {
2 // Creating the modal structure
3 const modal = document.createElement('div');
4 modal.className = 'dinosaur-modal';
5 modal.innerHTML = `
6 <div class="modal-content">
7 <div class="modal-header">
8 <h2>${dinosaurData.name}</h2>
9 <button class="close-btn">×</button>
10 </div>
11 <div class="modal-body">
12 <img src="${dinosaurData.image}" alt="${dinosaurData.name}">
13 <p><strong>Era:</strong> ${dinosaurData.era}</p>
14 <p><strong>Diet:</strong> ${dinosaurData.diet}</p>
15 <p><strong>Length:</strong> ${dinosaurData.length}m</p>
16 <p><strong>Weight:</strong> ${dinosaurData.weight}kg</p>
17 <p>${dinosaurData.description}</p>
18 </div>
19 <div class="modal-footer">
20 <button class="book-tour-btn">Book a Tour</button>
21 <button class="add-favorite-btn">Add to Favorites</button>
22 </div>
23 </div>
24 `;
25
26 // Get interactive elements from the modal
27 const closeBtn = modal.querySelector('.close-btn');
28 const bookTourBtn = modal.querySelector('.book-tour-btn');
29 const addFavoriteBtn = modal.querySelector('.add-favorite-btn');
30
31 // Close modal with X button
32 closeBtn.addEventListener('click', () => {
33 closeModal(modal);
34 });
35
36 // Close by clicking the background (overlay)
37 modal.addEventListener('click', (e) => {
38 if (e.target === modal) {
39 closeModal(modal);
40 }
41 });
42
43 // Book a tour
44 bookTourBtn.addEventListener('click', () => {
45 openBookingForm(dinosaurData.id);
46 });
47
48 // Add to favorites with button state change
49 addFavoriteBtn.addEventListener('click', () => {
50 addToFavorites(dinosaurData.id);
51 addFavoriteBtn.textContent = 'Added to favorites!';
52 addFavoriteBtn.disabled = true;
53 });
54
55 // Close with Escape key
56 const handleEscape = (e) => {
57 if (e.key === 'Escape') {
58 closeModal(modal);
59 document.removeEventListener('keydown', handleEscape);
60 }
61 };
62 document.addEventListener('keydown', handleEscape);
63
64 return modal;
65}
66
67function closeModal(modal) {
68 modal.classList.add('closing'); // Closing animation
69 setTimeout(() => modal.remove(), 300);
70}Notice several key practices:
document, but remove it when the modal disappears (prevents memory leaks)e.target === modal to distinguish a click on the overlay from a click on the modal contentA simple component for turning park systems on and off:
1function createToggle(label, onChange) {
2 const container = document.createElement('div');
3 container.className = 'toggle-container';
4 container.innerHTML = `
5 <span class="toggle-label">${label}</span>
6 <button class="toggle-btn" data-active="false">OFF</button>
7 `;
8
9 const btn = container.querySelector('.toggle-btn');
10
11 btn.addEventListener('click', () => {
12 const isActive = btn.dataset.active === 'true';
13 const newState = !isActive;
14
15 btn.dataset.active = String(newState);
16 btn.textContent = newState ? 'ON' : 'OFF';
17 btn.classList.toggle('active', newState);
18
19 // Call callback with new state
20 onChange(newState);
21 });
22
23 return container;
24}
25
26// Usage - fence control panel
27const fenceToggle = createToggle('Electric Fence', (active) => {
28 console.log(`Fence ${active ? 'ACTIVE' : 'DISABLED'}`);
29});
30document.getElementById('controls').appendChild(fenceToggle);Tabs are an essential element of the monitoring panel, allowing switching between views of different park zones:
1function createTabs(tabsData) {
2 const container = document.createElement('div');
3 container.className = 'tabs-container';
4
5 // Tab headers
6 const header = document.createElement('div');
7 header.className = 'tabs-header';
8
9 // Tab content
10 const body = document.createElement('div');
11 body.className = 'tabs-body';
12
13 tabsData.forEach((tab, index) => {
14 // Tab button
15 const tabBtn = document.createElement('button');
16 tabBtn.className = 'tab-btn' + (index === 0 ? ' active' : '');
17 tabBtn.textContent = tab.title;
18 tabBtn.dataset.index = index;
19
20 // Content panel
21 const tabPanel = document.createElement('div');
22 tabPanel.className = 'tab-panel' + (index === 0 ? ' active' : '');
23 tabPanel.innerHTML = tab.content;
24
25 // Click on tab
26 tabBtn.addEventListener('click', () => {
27 // Deactivate all
28 header.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
29 body.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
30
31 // Activate selected
32 tabBtn.classList.add('active');
33 tabPanel.classList.add('active');
34 });
35
36 header.appendChild(tabBtn);
37 body.appendChild(tabPanel);
38 });
39
40 container.appendChild(header);
41 container.appendChild(body);
42 return container;
43}
44
45// Usage
46const monitorTabs = createTabs([
47 { title: 'Zone A', content: '<p>T-Rex: status OK</p>' },
48 { title: 'Zone B', content: '<p>Raptors: active</p>' },
49 { title: 'Zone C', content: '<p>Brachiosaurus: calm</p>' },
50]);Every component should have a
destroy() method that removes all event listeners and prevents memory leaks:1class ParkWidget {
2 constructor(element) {
3 this.element = element;
4 this.handlers = [];
5 this.init();
6 }
7
8 addListener(target, event, handler) {
9 target.addEventListener(event, handler);
10 this.handlers.push({ target, event, handler });
11 }
12
13 destroy() {
14 // Remove all registered listeners
15 this.handlers.forEach(({ target, event, handler }) => {
16 target.removeEventListener(event, handler);
17 });
18 this.handlers = [];
19 this.element.remove();
20 }
21
22 init() {
23 this.addListener(this.element, 'click', (e) => {
24 console.log('Widget clicked');
25 });
26 this.addListener(document, 'keydown', (e) => {
27 if (e.key === 'Escape') this.destroy();
28 });
29 }
30}Storing references to all listeners and collectively removing them in
destroy() is the best practice when building components - especially important in dynamic applications where components are frequently created and destroyed.