W centrum kontroli Parku Jurajskiego każdy panel na ekranie to osobny komponent - karta dinozaura, okno alertu, formularz raportu. Każdy z nich reaguje na działania operatora: kliknięcia, klawisze, najazd myszą. Budowanie takich interaktywnych komponentów to jeden z najczęstszych scenariuszy użycia event listenerów w prawdziwych aplikacjach.
W tej lekcji nauczysz się tworzyć samodzielne, wielokrotnego użytku komponenty UI, które zarządzają własnymi wydarzeniami - od okien modalnych po panele z dynamiczną zawartością.
Modal to okno wyświetlane na wierzchu strony, blokujące interakcję z resztą interfejsu. W naszym parku użyjemy go do wyświetlania szczegółów dinozaura po kliknięciu jego karty.
1function createDinosaurModal(dinosaurData) {
2 // Tworzenie struktury modala
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>Dieta:</strong> ${dinosaurData.diet}</p>
15 <p><strong>Długość:</strong> ${dinosaurData.length}m</p>
16 <p><strong>Waga:</strong> ${dinosaurData.weight}kg</p>
17 <p>${dinosaurData.description}</p>
18 </div>
19 <div class="modal-footer">
20 <button class="book-tour-btn">Zarezerwuj wycieczkę</button>
21 <button class="add-favorite-btn">Dodaj do ulubionych</button>
22 </div>
23 </div>
24 `;
25
26 // Pobierz elementy interaktywne z modala
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 // Zamykanie modala przyciskiem X
32 closeBtn.addEventListener('click', () => {
33 closeModal(modal);
34 });
35
36 // Zamykanie kliknięciem w tło (overlay)
37 modal.addEventListener('click', (e) => {
38 if (e.target === modal) {
39 closeModal(modal);
40 }
41 });
42
43 // Rezerwacja wycieczki
44 bookTourBtn.addEventListener('click', () => {
45 openBookingForm(dinosaurData.id);
46 });
47
48 // Dodawanie do ulubionych ze zmianą stanu przycisku
49 addFavoriteBtn.addEventListener('click', () => {
50 addToFavorites(dinosaurData.id);
51 addFavoriteBtn.textContent = 'Dodano do ulubionych!';
52 addFavoriteBtn.disabled = true;
53 });
54
55 // Zamykanie klawiszem Escape
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'); // Animacja zamykania
69 setTimeout(() => modal.remove(), 300);
70}Zwróć uwagę na kilka kluczowych praktyk:
document, ale usuwamy go gdy modal znika (zapobiega wyciekom pamięci)e.target === modal żeby odróżnić kliknięcie w overlay od kliknięcia w treść modalaProsty komponent do włączania i wyłączania systemów parku:
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 // Wywołaj callback z nowym stanem
20 onChange(newState);
21 });
22
23 return container;
24}
25
26// Użycie - panel sterowania ogrodzeniami
27const fenceToggle = createToggle('Ogrodzenie elektryczne', (active) => {
28 console.log(`Ogrodzenie ${active ? 'AKTYWNE' : 'WYŁĄCZONE'}`);
29});
30document.getElementById('controls').appendChild(fenceToggle);Zakładki to niezbędny element panelu monitoringu, pozwalający przełączać między widokami różnych stref parku:
1function createTabs(tabsData) {
2 const container = document.createElement('div');
3 container.className = 'tabs-container';
4
5 // Nagłówki zakładek
6 const header = document.createElement('div');
7 header.className = 'tabs-header';
8
9 // Treści zakładek
10 const body = document.createElement('div');
11 body.className = 'tabs-body';
12
13 tabsData.forEach((tab, index) => {
14 // Przycisk zakładki
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 // Panel z treścią
21 const tabPanel = document.createElement('div');
22 tabPanel.className = 'tab-panel' + (index === 0 ? ' active' : '');
23 tabPanel.innerHTML = tab.content;
24
25 // Kliknięcie w zakładkę
26 tabBtn.addEventListener('click', () => {
27 // Dezaktywuj wszystkie
28 header.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
29 body.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));
30
31 // Aktywuj wybraną
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// Użycie
46const monitorTabs = createTabs([
47 { title: 'Strefa A', content: '<p>T-Rex: status OK</p>' },
48 { title: 'Strefa B', content: '<p>Raptory: aktywne</p>' },
49 { title: 'Strefa C', content: '<p>Brachiozaur: spokojny</p>' },
50]);Każdy komponent powinien mieć metodę
destroy(), która usuwa wszystkie event listenery i zapobiega wyciekom pamięci: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 // Usunięcie wszystkich zarejestrowanych listenerów
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 kliknięty');
25 });
26 this.addListener(document, 'keydown', (e) => {
27 if (e.key === 'Escape') this.destroy();
28 });
29 }
30}Przechowywanie referencji do wszystkich listenerów i ich zbiorowe usuwanie w
destroy() to najlepsza praktyka przy budowaniu komponentów - szczególnie ważna w dynamicznych aplikacjach, gdzie komponenty są często tworzone i niszczone.