JavaScript and TypeScript course Β· Module 8: Events and the DOM
Event Delegation
In this lesson8
"In Jurassic Park," Ray Arnold points to the central monitoring panel, "we don't install a separate alarm system for every dinosaur. That would be costly and inefficient. Instead, we have a central sector monitoring system that handles all enclosures in its area."
Ray opens the system diagram. "Event delegation works on the same principle. Instead of assigning hundreds of event listeners to individual elements, we assign one listener to their common parent. This parent 'delegates' handling to the appropriate elements."
What Is Event Delegation?
Event delegation is a technique that uses event propagation (bubbling) to handle events on many elements through a single listener assigned to their common ancestor. It's one of the most important and efficient techniques in JavaScript.
The Problem: Many Event Listeners
"Let's see the problem," says Ray, showing code in which every dinosaur card gets its own listener inside a forEach loop.
1// INEFFICIENT - separate listener for each card
2const dinosaurCards = document.querySelectorAll('.dinosaur-card');
3
4console.log(`Assigning listeners to ${dinosaurCards.length} dinosaur cards`);
5
6dinosaurCards.forEach((card, index) => {
7 card.addEventListener('click', function() {
8 const dinoId = this.dataset.dinoId;
9 const dinoName = this.dataset.dinoName;
10
11 console.log(`Dinosaur clicked: ${dinoName} (ID: ${dinoId})`);
12 showDinosaurDetails(dinoId);
13 });
14
15 console.log(`Listener ${index + 1}/${dinosaurCards.length} added`);
16});
17
18// Problems with this approach:
19// 1. If we have 100 cards = 100 listeners in memory
20// 2. Dynamically added cards WON'T have listeners
21// 3. Must manually remove listeners when removing cards
22// 4. Higher memory usage and slower pageIt works, but a hundred cards means a hundred listeners, and a card added later stays deaf, because the loop ran only once.
The Solution: Event Delegation
"Now watch," Ray shows the better solution: a single listener on the gallery works out by itself which card was clicked.
1// EFFICIENT - one listener on the container
2const dinosaurGallery = document.getElementById('dinosaur-gallery');
3
4dinosaurGallery.addEventListener('click', function(event) {
5 // Find the closest dinosaur card (even if a child of the card was clicked)
6 const card = event.target.closest('.dinosaur-card');
7
8 // If no dinosaur card was clicked, ignore
9 if (!card) return;
10
11 // Make sure the card is a child of our gallery
12 if (!dinosaurGallery.contains(card)) return;
13
14 const dinoId = card.dataset.dinoId;
15 const dinoName = card.dataset.dinoName;
16
17 console.log(`Dinosaur clicked: ${dinoName} (ID: ${dinoId})`);
18 showDinosaurDetails(dinoId);
19});
20
21// Advantages of this approach:
22// 1. Only ONE listener regardless of the number of cards
23// 2. Automatically handles dynamically added cards
24// 3. Better memory management
25// 4. Easier code maintenanceA click on a card's image bubbles all the way up to the gallery: event.target is the element that was clicked, and event.currentTarget is the element with the listener, that is, the gallery. The closest() method walks up from target and returns the matching card or null, while contains() checks that the card lives inside the gallery. The whole technique comes down to four steps: find the container, add a listener to it, check event.target, and run the action for the matching element.
Practical Example - Dinosaur Card System
"Let's build a complete system," says Ray. Here is the card structure and the start of a listener that recognizes the details and feeding buttons.
1// HTML structure:
2// <div id="dino-gallery">
3// <div class="dinosaur-card" data-dino-id="1" data-dino-name="T-Rex">
4// <img src="trex.jpg" alt="T-Rex">
5// <h3>Tyrannosaurus Rex</h3>
6// <div class="card-actions">
7// <button class="view-btn">Details</button>
8// <button class="feed-btn">Feed</button>
9// <button class="favorite-btn">β
</button>
10// </div>
11// </div>
12// ... more cards
13// </div>
14
15const gallery = document.getElementById('dino-gallery');
16
17gallery.addEventListener('click', function(event) {
18 const target = event.target;
19
20 // Handle details button
21 if (target.classList.contains('view-btn')) {
22 const card = target.closest('.dinosaur-card');
23 const dinoId = card.dataset.dinoId;
24 const dinoName = card.dataset.dinoName;
25
26 console.log(`Opening details for: ${dinoName}`);
27 openDinosaurModal(dinoId);
28 return; // Stop further checking
29 }
30
31 // Handle feed button
32 if (target.classList.contains('feed-btn')) {
33 const card = target.closest('.dinosaur-card');
34 const dinoId = card.dataset.dinoId;
35 const dinoName = card.dataset.dinoName;
36
37 console.log(`Starting feeding: ${dinoName}`);
38 feedDinosaur(dinoId);
39 return;
40 }We read the data-dino-id attribute as dataset.dinoId, because hyphens turn into camelCase. Every branch ends with return, so one click runs exactly one action. Next come favorites and the card itself.
1 // Handle favorite button
2 if (target.classList.contains('favorite-btn')) {
3 const card = target.closest('.dinosaur-card');
4 const dinoId = card.dataset.dinoId;
5 const dinoName = card.dataset.dinoName;
6
7 // Toggle favorite state
8 target.classList.toggle('active');
9
10 if (target.classList.contains('active')) {
11 console.log(`Added to favorites: ${dinoName}`);
12 addToFavorites(dinoId);
13 target.textContent = 'β
';
14 } else {
15 console.log(`Removed from favorites: ${dinoName}`);
16 removeFromFavorites(dinoId);
17 target.textContent = 'β';
18 }
19 return;
20 }
21
22 // Handle clicking the card itself (but not buttons)
23 const card = target.closest('.dinosaur-card');
24 if (card && gallery.contains(card)) {
25 const dinoId = card.dataset.dinoId;
26 const dinoName = card.dataset.dinoName;
27
28 console.log(`Card clicked: ${dinoName}`);
29 highlightCard(card);
30 }
31});The card is checked last, otherwise a click on a button would highlight it along the way. A classList.contains check is enough here, because the buttons contain plain text. An icon inside would take over the click and the condition would fail. Finally, the helpers.
1function openDinosaurModal(dinoId) {
2 console.log(`Loading dinosaur data ID: ${dinoId}`);
3 // Modal opening logic
4}
5
6function feedDinosaur(dinoId) {
7 console.log(`Activating feeding system for dinosaur ID: ${dinoId}`);
8 console.log('Checking security...');
9 console.log('Opening dispenser...');
10 console.log('Feeding completed');
11}
12
13function addToFavorites(dinoId) {
14 const favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
15 if (!favorites.includes(dinoId)) {
16 favorites.push(dinoId);
17 localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
18 console.log('Saved to favorites');
19 }
20}
21
22function removeFromFavorites(dinoId) {
23 let favorites = JSON.parse(localStorage.getItem('favoriteDinosaurs') || '[]');
24 favorites = favorites.filter(id => id !== dinoId);
25 localStorage.setItem('favoriteDinosaurs', JSON.stringify(favorites));
26 console.log('Removed from favorites');
27}
28
29function highlightCard(card) {
30 // Remove highlight from other cards
31 document.querySelectorAll('.dinosaur-card.highlighted').forEach(c => {
32 c.classList.remove('highlighted');
33 });
34
35 // Add highlight to the clicked card
36 card.classList.add('highlighted');
37 console.log('Card highlighted');
38}The helpers know nothing about events. The || '[]' fallback saves the very first visit, when getItem() returns null.
Dynamically Adding Elements
"The greatest advantage of delegation," Ray adds new elements to the interface, "is automatic handling of dynamically added elements."
1const gallery = document.getElementById('dino-gallery');
2
3// Event delegation - works for all cards, even future ones
4gallery.addEventListener('click', function(event) {
5 const card = event.target.closest('.dinosaur-card');
6 if (!card) return;
7
8 const dinoName = card.dataset.dinoName;
9 console.log(`Clicked: ${dinoName}`);
10});
11
12// Function to dynamically add new cards
13function addNewDinosaur(dinosaur) {
14 const newCard = document.createElement('div');
15 newCard.className = 'dinosaur-card';
16 newCard.dataset.dinoId = dinosaur.id;
17 newCard.dataset.dinoName = dinosaur.name;
18
19 newCard.innerHTML = `
20 <img src="${dinosaur.image}" alt="${dinosaur.name}">
21 <h3>${dinosaur.name}</h3>
22 <p>${dinosaur.species}</p>
23 <div class="card-actions">
24 <button class="view-btn">Details</button>
25 <button class="feed-btn">Feed</button>
26 <button class="favorite-btn">β</button>
27 </div>
28 `;
29
30 gallery.appendChild(newCard);
31 console.log(`New dinosaur added: ${dinosaur.name}`);
32
33 // NO NEED to add event listeners!
34 // Delegation automatically handles the new card!
35}
36
37// Example usage
38setTimeout(() => {
39 addNewDinosaur({
40 id: '15',
41 name: 'Spinosaurus',
42 species: 'Spinosaurus aegyptiacus',
43 image: 'spinosaurus.jpg'
44 });
45
46 // The new card already works with clicks!
47}, 3000);There isn't a single addEventListener in addNewDinosaur(), yet the Spinosaurus added after three seconds responds to clicks right away, because they bubble up to the listener that has been waiting on the gallery from the start.
Advanced Delegation - Filters and Conditions
"Sometimes," Ray explains, "we need more precise control." Every button on the panel says what it should do through data-action, and the listener hands out the tasks.
1const parkDashboard = document.getElementById('park-dashboard');
2
3parkDashboard.addEventListener('click', function(event) {
4 const target = event.target;
5
6 // Delegation with data-action attribute
7 const action = target.dataset.action;
8
9 if (action) {
10 console.log(`Action: ${action}`);
11
12 switch(action) {
13 case 'open-gate':
14 handleOpenGate(target);
15 break;
16
17 case 'close-gate':
18 handleCloseGate(target);
19 break;
20
21 case 'start-feeding':
22 handleStartFeeding(target);
23 break;
24
25 case 'emergency-protocol':
26 handleEmergencyProtocol(target);
27 break;
28
29 case 'view-stats':
30 handleViewStats(target);
31 break;
32
33 default:
34 console.log('Unknown action:', action);
35 }
36
37 return;
38 }A new action means a new case, not a new listener. When the element has no action, the listener checks classes with the matches() method and tags through tagName, which returns the name in uppercase, hence 'A'.
1 // Delegation with classes
2 if (target.matches('.enclosure-zone')) {
3 const enclosureId = target.dataset.enclosureId;
4 console.log(`Enclosure zone clicked: ${enclosureId}`);
5 showEnclosureDetails(enclosureId);
6 return;
7 }
8
9 // Delegation with tags
10 if (target.tagName === 'A' && target.classList.contains('internal-link')) {
11 event.preventDefault();
12 const href = target.getAttribute('href');
13 console.log(`Internal navigation: ${href}`);
14 navigateInternal(href);
15 return;
16 }
17});
18
19function handleOpenGate(button) {
20 const gateId = button.dataset.gateId;
21 const enclosureId = button.dataset.enclosureId;
22
23 console.log(`Opening gate ${gateId} in enclosure ${enclosureId}`);
24 console.log('Checking security status...');
25 console.log('Gate opened');
26}
27
28function handleCloseGate(button) {
29 const gateId = button.dataset.gateId;
30 console.log(`Closing gate ${gateId}`);
31 console.log('Gate closed and secured');
32}
33
34function handleStartFeeding(button) {
35 const enclosureId = button.dataset.enclosureId;
36 const feedType = button.dataset.feedType;
37
38 console.log(`Starting feeding in enclosure ${enclosureId}`);
39 console.log(`Food type: ${feedType}`);
40}
41
42function handleEmergencyProtocol(button) {
43 const protocolId = button.dataset.protocolId;
44
45 console.log('ALARM! Activating emergency protocol');
46 console.log(`Protocol: ${protocolId}`);
47 console.log('Closing all gates...');
48 console.log('Notifying staff...');
49}Calling preventDefault() blocks the link's normal navigation so that the panel can switch the view itself. Every handler reads the button's data through dataset: the markup says what to do, the functions know how.
Performance - Comparison
Ray shows performance tests. "Numbers don't lie, but everyone has to measure them on their own machine." The console.time() and console.timeEnd() functions measure the time between two calls with the same label.
1// Test 1: Without delegation (separate listeners)
2console.time('Assignment without delegation');
3
4const cards1 = document.querySelectorAll('.card');
5cards1.forEach(card => {
6 card.addEventListener('click', function() {
7 console.log('Clicked');
8 });
9});
10
11console.timeEnd('Assignment without delegation');
12// Time grows with the number of cards: 100 cards = 100 addEventListener calls
13
14// Test 2: With delegation (one listener)
15console.time('Assignment with delegation');
16
17const container = document.getElementById('container');
18container.addEventListener('click', function(event) {
19 if (event.target.matches('.card')) {
20 console.log('Clicked');
21 }
22});
23
24console.timeEnd('Assignment with delegation');
25// One addEventListener call, regardless of the number of cards
26
27// Memory usage:
28// Without delegation: a separate listener and a separate function for every card
29// With delegation: one listener on the container
30
31console.log('Number of listeners for 100 cards:');
32console.log('Without delegation: 100');
33console.log('With delegation: 1');
34console.log('Measure exact time and memory in your browser DevTools');We deliberately don't quote milliseconds or kilobytes, because they depend on the browser and the hardware. One thing is certain: without delegation, every new card adds another listener, and with delegation there is only one.
matches() vs closest() - Choosing the Method
"The two most important methods for delegation," Ray explains. Both take a CSS selector, but they look in different directions of the tree.
1const container = document.getElementById('enclosure-list');
2
3container.addEventListener('click', function(event) {
4 const target = event.target;
5
6 // matches() - checks if the element ITSELF matches
7 if (target.matches('.enclosure-item')) {
8 console.log('Clicked DIRECTLY on .enclosure-item');
9 // Works only if exactly the element with the class was clicked
10 }
11
12 // closest() - searches upward to the first match
13 const item = target.closest('.enclosure-item');
14 if (item) {
15 console.log('Clicked on .enclosure-item OR its child');
16 // Works if the element or any of its children were clicked
17 }
18});
19
20// HTML:
21// <div class="enclosure-item">
22// <h3>T-Rex Enclosure</h3> β matches() NO, closest() YES
23// <p>Status: Active</p> β matches() NO, closest() YES
24// <button>Details</button> β matches() NO, closest() YES
25// </div> β matches() YES, closest() YES
26
27// CONCLUSION: Use closest() for delegation - it's more flexibleThe matches() method checks only the element itself, while closest() starts with it and walks up. That's why I recommend closest() as the default choice for delegation.
Complete Park Management System
"Let's combine everything into a working system," says Ray. One listener on the dashboard passes each click on to the class methods.
1class JurassicParkManagementSystem {
2 constructor() {
3 this.selectedEnclosures = new Set();
4 this.initialize();
5 }
6
7 initialize() {
8 console.log('Initializing park management system');
9 this.setupDelegation();
10 }
11
12 setupDelegation() {
13 // One main listener for the entire dashboard
14 const dashboard = document.getElementById('park-dashboard');
15
16 dashboard.addEventListener('click', (event) => {
17 this.handleDashboardClick(event);
18 });
19
20 console.log('Delegation system active');
21 }
22
23 handleDashboardClick(event) {
24 const target = event.target;
25
26 // Handle different types of clicks
27 this.handleActionButton(target) ||
28 this.handleTabNavigation(target) ||
29 this.handleStatusIndicator(target) ||
30 this.handleEnclosureSelection(target);
31 }Each method returns true when the click belongs to it, and the || operator stops at the first true. That's why we ask the buttons first and the whole card last, just like in the gallery above. The arrow function keeps this pointing at the class instance.
1 handleEnclosureSelection(target) {
2 const enclosure = target.closest('.enclosure-card');
3 if (!enclosure) return false;
4
5 const enclosureId = enclosure.dataset.enclosureId;
6 const enclosureName = enclosure.dataset.enclosureName;
7
8 console.log(`Enclosure selected: ${enclosureName} (ID: ${enclosureId})`);
9
10 // Toggle selection
11 if (this.selectedEnclosures.has(enclosureId)) {
12 this.selectedEnclosures.delete(enclosureId);
13 enclosure.classList.remove('selected');
14 console.log('Removed from selection');
15 } else {
16 this.selectedEnclosures.add(enclosureId);
17 enclosure.classList.add('selected');
18 console.log('Added to selection');
19 }
20
21 this.updateSelectionDisplay();
22 return true;
23 }
24
25 handleActionButton(target) {
26 const action = target.dataset.action;
27 if (!action) return false;
28
29 const enclosure = target.closest('.enclosure-card');
30 const enclosureId = enclosure?.dataset.enclosureId;
31
32 console.log(`Action: ${action} for enclosure: ${enclosureId}`);
33
34 const actions = {
35 'feed': () => this.feedDinosaur(enclosureId),
36 'clean': () => this.cleanEnclosure(enclosureId),
37 'maintenance': () => this.scheduleMaintenance(enclosureId),
38 'emergency': () => this.activateEmergency(enclosureId)
39 };
40
41 const handler = actions[action];
42 if (handler) {
43 handler();
44 return true;
45 }
46
47 return false;
48 }A Set stores the selection without duplicates, the actions object replaces a switch, and ?. prevents an error when the button isn't inside a card. Tabs and status indicators work the same way.
1 handleTabNavigation(target) {
2 if (!target.classList.contains('tab-button')) return false;
3
4 const tabName = target.dataset.tab;
5 console.log(`Switching to tab: ${tabName}`);
6
7 // Remove active from all tabs
8 document.querySelectorAll('.tab-button').forEach(tab => {
9 tab.classList.remove('active');
10 });
11
12 // Add active to the clicked tab
13 target.classList.add('active');
14
15 // Show appropriate content
16 this.showTabContent(tabName);
17 return true;
18 }
19
20 handleStatusIndicator(target) {
21 if (!target.classList.contains('status-indicator')) return false;
22
23 const enclosure = target.closest('.enclosure-card');
24 const enclosureId = enclosure?.dataset.enclosureId;
25
26 console.log(`Showing status details for enclosure: ${enclosureId}`);
27 this.showStatusDetails(enclosureId);
28 return true;
29 }None of these elements has its own listener, because their clicks reach the dashboard anyway. At the end come the worker methods and the system start.
1 feedDinosaur(enclosureId) {
2 console.log(`Feeding dinosaur in enclosure: ${enclosureId}`);
3 }
4
5 cleanEnclosure(enclosureId) {
6 console.log(`Cleaning enclosure: ${enclosureId}`);
7 }
8
9 scheduleMaintenance(enclosureId) {
10 console.log(`Scheduling maintenance for enclosure: ${enclosureId}`);
11 }
12
13 activateEmergency(enclosureId) {
14 console.log(`ALARM! Emergency protocol for enclosure: ${enclosureId}`);
15 }
16
17 showTabContent(tabName) {
18 console.log(`Loading tab content: ${tabName}`);
19 }
20
21 showStatusDetails(enclosureId) {
22 console.log(`Enclosure status details: ${enclosureId}`);
23 }
24
25 updateSelectionDisplay() {
26 const count = this.selectedEnclosures.size;
27 const display = document.getElementById('selection-counter');
28
29 if (display) {
30 display.textContent = `Selected: ${count} enclosures`;
31 }
32
33 console.log(`Currently selected enclosures: ${Array.from(this.selectedEnclosures).join(', ')}`);
34 }
35}
36
37// System initialization
38const parkSystem = new JurassicParkManagementSystem();A single listener guards the whole dashboard, no matter how many enclosures appear on the screen.
Summary
"Event delegation," Ray concludes, "is not just an optimization. It's a fundamental technique in modern JavaScript. One listener handling thousands of elements - it's like a central monitoring system handling the entire park. Efficient, scalable, and easy to maintain."
Key rules:
- Use
closest()to find target elements - Check if the element is a child of the container with the listener
- Use data-attributes for actions and identifiers
- Remember to
returnafter handling an event - Delegation automatically works with dynamic elements
- Saves memory and improves performance
In the sandbox, the dragstart listener sits on the whole document, so the dinosaurs added every ten seconds can be dragged right away. Next lesson: mouse and keyboard events.
Remember: one sensor in the sector control center hears every enclosure, including the one built yesterday.
Code for this lesson: 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>Drag & Drop - Moving Dinosaurs</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: 30px;
24 }
25
26 .instructions {
27 background: rgba(52, 152, 219, 0.2);
28 border: 1px solid #3498db;
29 border-radius: 10px;
30 padding: 20px;
31 margin-bottom: 30px;
32 text-align: center;
33 }
34
35 .enclosures-container {
36 display: grid;
37 grid-template-columns: 1fr 1fr;
38 gap: 30px;
39 margin-bottom: 30px;
40 }
41
42 .enclosure {
43 background: rgba(0, 0, 0, 0.6);
44 border: 3px dashed #7f8c8d;
45 border-radius: 15px;
46 padding: 20px;
47 min-height: 300px;
48 transition: all 0.3s ease;
49 }
50
51 .enclosure.drag-over {
52 border-color: #3498db;
53 background: rgba(52, 152, 219, 0.2);
54 border-style: solid;
55 transform: scale(1.02);
56 }
57
58 .enclosure h3 {
59 text-align: center;
60 margin-bottom: 20px;
61 color: #ecf0f1;
62 }
63
64 .herbivore-enclosure {
65 border-color: #27ae60;
66 }
67
68 .carnivore-enclosure {
69 border-color: #e74c3c;
70 }
71
72 .herbivore-enclosure.drag-over {
73 border-color: #2ecc71;
74 background: rgba(46, 204, 113, 0.2);
75 }
76
77 .carnivore-enclosure.drag-over {
78 border-color: #e74c3c;
79 background: rgba(231, 76, 60, 0.2);
80 }
81
82 .dinosaur-card {
83 background: linear-gradient(45deg, #34495e, #2c3e50);
84 border-radius: 10px;
85 padding: 15px;
86 margin: 10px 0;
87 cursor: grab;
88 transition: all 0.3s ease;
89 border: 2px solid transparent;
90 user-select: none;
91 }
92
93 .dinosaur-card:hover {
94 transform: translateY(-3px);
95 box-shadow: 0 10px 25px rgba(0, 0, 0, 0.3);
96 }
97
98 .dinosaur-card.dragging {
99 opacity: 0.5;
100 transform: rotate(5deg);
101 cursor: grabbing;
102 }
103
104 .dinosaur-card.herbivore {
105 border-color: #27ae60;
106 }
107
108 .dinosaur-card.carnivore {
109 border-color: #e74c3c;
110 }
111
112 .dino-header {
113 display: flex;
114 align-items: center;
115 gap: 10px;
116 margin-bottom: 10px;
117 }
118
119 .dino-emoji {
120 font-size: 24px;
121 }
122
123 .dino-name {
124 font-weight: bold;
125 color: #ecf0f1;
126 }
127
128 .dino-info {
129 font-size: 14px;
130 color: #bdc3c7;
131 margin: 5px 0;
132 }
133
134 .dino-diet {
135 display: inline-block;
136 padding: 3px 8px;
137 border-radius: 12px;
138 font-size: 12px;
139 font-weight: bold;
140 }
141
142 .herbivore .dino-diet {
143 background: #27ae60;
144 color: white;
145 }
146
147 .carnivore .dino-diet {
148 background: #e74c3c;
149 color: white;
150 }
151
152 .drag-instructions {
153 text-align: center;
154 color: #bdc3c7;
155 font-style: italic;
156 margin: 20px 0;
157 }
158
159 .stats-panel {
160 background: rgba(0, 0, 0, 0.8);
161 border-radius: 10px;
162 padding: 20px;
163 margin-top: 30px;
164 }
165
166 .stats-grid {
167 display: grid;
168 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
169 gap: 20px;
170 }
171
172 .stat-item {
173 background: rgba(255, 255, 255, 0.1);
174 padding: 15px;
175 border-radius: 8px;
176 text-align: center;
177 }
178
179 .stat-value {
180 font-size: 24px;
181 font-weight: bold;
182 color: #3498db;
183 }
184
185 .stat-label {
186 color: #bdc3c7;
187 font-size: 14px;
188 margin-top: 5px;
189 }
190
191 .danger-warning {
192 background: #e74c3c;
193 color: white;
194 padding: 10px;
195 border-radius: 8px;
196 margin: 10px 0;
197 text-align: center;
198 font-weight: bold;
199 display: none;
200 }
201
202 @media (max-width: 768px) {
203 .enclosures-container {
204 grid-template-columns: 1fr;
205 }
206 }
207 </style>
208</head>
209<body>
210 <div class="container">
211 <h1>Moving Dinosaurs Between Enclosures</h1>
212
213 <div class="instructions">
214 <strong>Task:</strong> Drag dinosaurs to the appropriate enclosures based on their diet.<br>
215 <span style="color: #27ae60;">β </span> Herbivores β Safe Enclosure |
216 <span style="color: #e74c3c;">β </span> Carnivores β High Security Enclosure
217 </div>
218
219 <div class="enclosures-container">
220 <!-- Enclosure for herbivores -->
221 <div class="enclosure herbivore-enclosure" data-diet="herbivore" id="herbivore-enclosure">
222 <h3>Safe Enclosure (Herbivores)</h3>
223 <div class="drag-instructions">Drag herbivores here</div>
224
225 <!-- Initial herbivore dinosaurs -->
226 <div class="dinosaur-card herbivore" draggable="true" data-dino="triceratops" data-diet="herbivore">
227 <div class="dino-header">
228 <span class="dino-emoji">β</span>
229 <span class="dino-name">Triceratops</span>
230 </div>
231 <div class="dino-info">Length: 9m | Era: Cretaceous</div>
232 <span class="dino-diet">Herbivore</span>
233 </div>
234
235 <div class="dinosaur-card herbivore" draggable="true" data-dino="brachiosaurus" data-diet="herbivore">
236 <div class="dino-header">
237 <span class="dino-emoji">β</span>
238 <span class="dino-name">Brachiosaurus</span>
239 </div>
240 <div class="dino-info">Length: 23m | Era: Jurassic</div>
241 <span class="dino-diet">Herbivore</span>
242 </div>
243 </div>
244
245 <!-- Enclosure for carnivores -->
246 <div class="enclosure carnivore-enclosure" data-diet="carnivore" id="carnivore-enclosure">
247 <h3>High Security Enclosure (Carnivores)</h3>
248 <div class="drag-instructions">Drag carnivores here</div>
249 <div class="danger-warning" id="danger-warning">
250 WARNING! Mixing carnivores and herbivores is dangerous!
251 </div>
252
253 <!-- Initial carnivore dinosaurs -->
254 <div class="dinosaur-card carnivore" draggable="true" data-dino="trex" data-diet="carnivore">
255 <div class="dino-header">
256 <span class="dino-emoji">β</span>
257 <span class="dino-name">Tyrannosaurus Rex</span>
258 </div>
259 <div class="dino-info">Length: 13m | Era: Cretaceous</div>
260 <span class="dino-diet">Carnivore</span>
261 </div>
262
263 <div class="dinosaur-card carnivore" draggable="true" data-dino="velociraptor" data-diet="carnivore">
264 <div class="dino-header">
265 <span class="dino-emoji">β</span>
266 <span class="dino-name">Velociraptor</span>
267 </div>
268 <div class="dino-info">Length: 2m | Era: Cretaceous</div>
269 <span class="dino-diet">Carnivore</span>
270 </div>
271 </div>
272 </div>
273
274 <!-- Statistics panel -->
275 <div class="stats-panel">
276 <h3>Enclosure Statistics</h3>
277 <div class="stats-grid">
278 <div class="stat-item">
279 <div class="stat-value" id="herbivore-count">2</div>
280 <div class="stat-label">Herbivores</div>
281 </div>
282 <div class="stat-item">
283 <div class="stat-value" id="carnivore-count">2</div>
284 <div class="stat-label">Carnivores</div>
285 </div>
286 <div class="stat-item">
287 <div class="stat-value" id="total-moves">0</div>
288 <div class="stat-label">Total transfers</div>
289 </div>
290 <div class="stat-item">
291 <div class="stat-value" id="safety-status">OK</div>
292 <div class="stat-label">Security status</div>
293 </div>
294 </div>
295 </div>
296 </div>
297
298 <script>
299 let draggedElement = null;
300 let totalMoves = 0;
301
302 // Get references to elements
303 const herbivoreEnclosure = document.getElementById('herbivore-enclosure');
304 const carnivoreEnclosure = document.getElementById('carnivore-enclosure');
305 const dangerWarning = document.getElementById('danger-warning');
306 const herbivoreCount = document.getElementById('herbivore-count');
307 const carnivoreCount = document.getElementById('carnivore-count');
308 const totalMovesCount = document.getElementById('total-moves');
309 const safetyStatus = document.getElementById('safety-status');
310
311 // Event listeners for dragged element
312 document.addEventListener('dragstart', function(e) {
313 if (e.target.classList.contains('dinosaur-card')) {
314 draggedElement = e.target;
315 e.target.classList.add('dragging');
316
317 console.log(`Started dragging: ${e.target.dataset.dino}`);
318
319 // Setting the data for the transfer
320 e.dataTransfer.setData('text/plain', e.target.dataset.dino);
321 e.dataTransfer.effectAllowed = 'move';
322 }
323 });
324
325 document.addEventListener('dragend', function(e) {
326 if (e.target.classList.contains('dinosaur-card')) {
327 e.target.classList.remove('dragging');
328 draggedElement = null;
329 }
330 });
331
332 // Event listeners for drop areas
333 document.querySelectorAll('.enclosure').forEach(enclosure => {
334 enclosure.addEventListener('dragover', function(e) {
335 e.preventDefault(); // Allowing the drop
336 e.dataTransfer.dropEffect = 'move';
337
338 if (!this.classList.contains('drag-over')) {
339 this.classList.add('drag-over');
340 }
341 });
342
343 enclosure.addEventListener('dragleave', function(e) {
344 // Check if we're actually leaving the enclosure
345 if (!this.contains(e.relatedTarget)) {
346 this.classList.remove('drag-over');
347 }
348 });
349
350 enclosure.addEventListener('drop', function(e) {
351 e.preventDefault();
352 this.classList.remove('drag-over');
353
354 if (draggedElement) {
355 const draggedDiet = draggedElement.dataset.diet;
356 const targetDiet = this.dataset.diet;
357 const dinoName = draggedElement.querySelector('.dino-name').textContent;
358
359 // Checking whether the dinosaur matches the enclosure
360 if (draggedDiet === targetDiet) {
361 // Moving the element
362 this.appendChild(draggedElement);
363 totalMoves++;
364
365 console.log(`${dinoName} moved to the right enclosure!`);
366
367 // Success animation
368 const movedCard = draggedElement; // dragend resets draggedElement before the second is up
369 movedCard.style.border = '2px solid #27ae60';
370 setTimeout(() => {
371 movedCard.style.border = '';
372 }, 1000);
373
374 } else {
375 // Invalid transfer
376 console.log(`ERROR: ${dinoName} cannot be in this enclosure!`);
377
378 // Error animation
379 const refusedCard = draggedElement;
380 refusedCard.style.border = '2px solid #e74c3c';
381 refusedCard.style.background = 'rgba(231, 76, 60, 0.3)';
382
383 setTimeout(() => {
384 refusedCard.style.border = '';
385 refusedCard.style.background = '';
386 }, 1000);
387
388 // Show warning
389 alert(`WARNING!\n\n${dinoName} cannot be placed in this enclosure!\n${draggedDiet === 'carnivore' ? 'Carnivores' : 'Herbivores'} must be in the appropriate enclosure for the park's safety.`);
390 }
391
392 updateStats();
393 }
394 });
395 });
396
397 // Function that updates the statistics
398 function updateStats() {
399 const herbivores = document.querySelectorAll('#herbivore-enclosure .dinosaur-card').length;
400 const carnivores = document.querySelectorAll('#carnivore-enclosure .dinosaur-card').length;
401
402 herbivoreCount.textContent = herbivores;
403 carnivoreCount.textContent = carnivores;
404 totalMovesCount.textContent = totalMoves;
405
406 // Checking safety
407 const herbivoreEnclosureHasCarnivores = document.querySelectorAll('#herbivore-enclosure .carnivore').length > 0;
408 const carnivoreEnclosureHasHerbivores = document.querySelectorAll('#carnivore-enclosure .herbivore').length > 0;
409
410 const isDangerous = herbivoreEnclosureHasCarnivores || carnivoreEnclosureHasHerbivores;
411
412 if (isDangerous) {
413 safetyStatus.textContent = '!';
414 safetyStatus.style.color = '#e74c3c';
415 dangerWarning.style.display = 'block';
416 } else {
417 safetyStatus.textContent = 'OK';
418 safetyStatus.style.color = '#27ae60';
419 dangerWarning.style.display = 'none';
420 }
421
422 // Check if all dinosaurs are in the correct enclosures
423 const allInCorrectEnclosures = !isDangerous;
424 if (allInCorrectEnclosures && totalMoves > 0) {
425 setTimeout(() => {
426 alert('Congratulations!\n\nAll dinosaurs are now in the correct enclosures.\nThe park is safe!');
427 }, 500);
428 }
429 }
430
431 // Adding new dinosaurs after a few seconds
432 function addNewDinosaur() {
433 const newDinos = [
434 {
435 name: 'Stegosaurus',
436 emoji: '',
437 diet: 'herbivore',
438 length: '9m',
439 era: 'Jurassic'
440 },
441 {
442 name: 'Allosaurus',
443 emoji: '',
444 diet: 'carnivore',
445 length: '9m',
446 era: 'Jurassic'
447 }
448 ];
449
450 const randomDino = newDinos[Math.floor(Math.random() * newDinos.length)];
451
452 const newDinoCard = document.createElement('div');
453 newDinoCard.className = `dinosaur-card ${randomDino.diet}`;
454 newDinoCard.draggable = true;
455 newDinoCard.dataset.dino = randomDino.name.toLowerCase();
456 newDinoCard.dataset.diet = randomDino.diet;
457
458 newDinoCard.innerHTML = `
459 <div class="dino-header">
460 <span class="dino-emoji">${randomDino.emoji}</span>
461 <span class="dino-name">${randomDino.name}</span>
462 </div>
463 <div class="dino-info">Length: ${randomDino.length} | Era: ${randomDino.era}</div>
464 <span class="dino-diet">${randomDino.diet === 'herbivore' ? 'Herbivore' : 'Carnivore'}</span>
465 `;
466
467 // Add to incorrect enclosures for challenge
468 if (randomDino.diet === 'herbivore') {
469 carnivoreEnclosure.appendChild(newDinoCard);
470 } else {
471 herbivoreEnclosure.appendChild(newDinoCard);
472 }
473
474 updateStats();
475
476 console.log(`Added a new dinosaur: ${randomDino.name}`);
477 }
478
479 // Adding a new dinosaur every 10 seconds
480 setInterval(addNewDinosaur, 10000);
481
482 // Initial statistics update
483 updateStats();
484
485 // Touch events for mobile devices (basic support)
486 let touchItem = null;
487 let touchOffset = { x: 0, y: 0 };
488
489 document.addEventListener('touchstart', function(e) {
490 if (e.target.classList.contains('dinosaur-card')) {
491 touchItem = e.target;
492 const touch = e.touches[0];
493 const rect = touchItem.getBoundingClientRect();
494
495 touchOffset.x = touch.clientX - rect.left;
496 touchOffset.y = touch.clientY - rect.top;
497
498 touchItem.style.position = 'fixed';
499 touchItem.style.zIndex = '1000';
500 touchItem.classList.add('dragging');
501
502 e.preventDefault();
503 }
504 });
505
506 document.addEventListener('touchmove', function(e) {
507 if (touchItem) {
508 const touch = e.touches[0];
509 touchItem.style.left = (touch.clientX - touchOffset.x) + 'px';
510 touchItem.style.top = (touch.clientY - touchOffset.y) + 'px';
511
512 e.preventDefault();
513 }
514 });
515
516 document.addEventListener('touchend', function(e) {
517 if (touchItem) {
518 const touch = e.changedTouches[0];
519 const elementBelow = document.elementFromPoint(touch.clientX, touch.clientY);
520 const enclosure = elementBelow ? elementBelow.closest('.enclosure') : null;
521
522 if (enclosure) {
523 draggedElement = touchItem;
524
525 // Simulating a drop event
526 const dropEvent = new Event('drop');
527 enclosure.dispatchEvent(dropEvent);
528 }
529
530 // Reset styles
531 touchItem.style.position = '';
532 touchItem.style.left = '';
533 touchItem.style.top = '';
534 touchItem.style.zIndex = '';
535 touchItem.classList.remove('dragging');
536
537 touchItem = null;
538 draggedElement = null;
539 }
540 });
541 </script>
542</body>
543</html>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 are the main advantages of event delegation?
2. What is the difference between event.target and event.currentTarget?
Hands-on tasks in the game
- Code editor
The index.html file has a list of dinosaur cards #dino-list and a button that adds a new card after the page has loaded. Use event delegation: replace ___BLANK1___ with the variable of the container that holds the only click listener, and ___BLANK2___ with the method that starts at event.target and looks for the closest element matching the .dino-card selector. Clicking a card (also its inside and a card added later) should show the dinosaur name in #selected-dino, while the reserve card outside the list should not react.
- Vertical ordering
Arrange the steps for configuring event delegation:
- Horizontal ordering
Arrange the event delegation elements: