JavaScript and TypeScript course Β· Module 8: Events and the DOM

Event Delegation

12 min read
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 page

It 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 maintenance

A 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 flexible

The 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 return after 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. 1. What are the main advantages of event delegation?

  2. 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:

Useful articles