Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Pracujemy nad nową wersją strony! ✅ Sprawdź jak będzie wyglądać!
Chcesz wyświetlać informacje pobrane z serwera i tworzyć interaktywne elementy listy? Naucz się wykorzystywać propagację! Delegacja zdarzeń w kontekście API i elementów tworzonych dynamicznie to zagadnienie, które junior powinien poznać jak najszybciej.
Są to elementy HTML tworzone przy pomocy JavaScriptu już po załadowaniu drzewa DOM – czyli strony z naszym kodem HTML. Przykładem może być tutaj zwykła to-do lista czy formularz dodawania
członków klubu do listy <ul />: wypełniamy dane, klikamy „dodaj” i kolejna osoba trafia do rozpiski jako element <li />. Ten dodany element listy to właśnie element HTML stworzony dynamicznie.
Innym przypadkiem jest pobieranie danych z serwera. Jeżeli np. chcemy wyświetlić na stronie listę książek, to po otrzymaniu odpowiedzi z API (zapewne tablicy obiektów z informacjami o książkach), utworzymy odpowiednie elementy – mogą to być zdjęcia okładek opatrzone tytułem i opisem.
Nasłuchiwanie zdarzenia przez dany element to oczekiwanie na nastąpienie konkretnej sytuacji, np. kliknięcia, załadowania się filmu, naciśnięcia klawisza, scrollowania itd. W kodzie wygląda ono w ten sposób:
document.addEventListener('DOMContentLoaded', function () {
console.log('Hello world');
});
W powyższym przykładzie oczekujemy zdarzenia DOMContentLoaded, czyli załadowania się drzewa DOM. Dopiero po wystąpieniu zdarzenia uruchomi się funkcja wyświetlająca w konsoli napis „Hello world”. Przykład ten doskonale pokazuje, że eventy nie zawsze związane są z akcjami użytkownika, jak znane Ci już pewnie zdarzenia np. click czy keydown.
Propagacja to rozchodzenie się zdarzenia. Gdyby porównać elementy HTML do ustawionych w rzędzie członków rodziny, to rozejściem się zdarzenia można by nazwać taką sytuację:
Pradziadek czeka na zdarzenie „podano obiad”. Stoi on na początku rzędu, dalej stoi jego syn, potem wnuczka i prawnuczka. Obojętnie, które z nich powie „podano obiad”, to pradziadek i tak to usłyszy. Co więcej, będzie wiedział, kto jest autorem zawołania i będzie mógł odpowiedzieć: „dziękuję za informację, Mariuszu/Zofio/Zuzio”. Nawet więcej! Będzie mógł zarządzić, by wszyscy jego potomkowie poszli do jadalni 😉
Właśnie to, że znamy „autora zawołania” wykorzystamy za chwilę w przykładzie. Najpierw jednak omówimy dwa podejścia, które nie są optymalne (a jedno z nich nie działa!), lecz często pojawiają się w rozwiązaniach początkujących programistów.
Przedstawię Ci to na przykładzie „panelu administratora”, gdzie za pomocą przycisku dodajemy nowy produkt do listy. Dla ułatwienia przekazu nie budowałem formularza danych produktu – potraktujmy nasz button jako ostatni etap dodawania informacji o produkcie.
W pliku index.html przygotowałem już odpowiednią strukturę. Mamy tam przycisk „Dodaj produkt” oraz listę, na której będziemy umieszczać kolejne elementy. Zwróć uwagę na element listy <li /> o klasie .adminPanel__item--template – wykorzystam go zaraz jako szablon.
<main class="articleExamples">
<h1 class="articleExamples__header">
Rozwiązanie przed dodaniem nasłuchiwania na przycisku „Usuń”
</h1>
<section class="articleExamples__adminPanel adminPanel">
<button class="adminPanel__addProductBtn">Dodaj produkt</button>
<ul class="adminPanel__list">
<li class="adminPanel__item adminPanel__item--template">
<article class="adminPanel__container product">
<h2 class="product__name"></h2>
<button class="product__deleteBtn">usuń</button>
</article>
</li>
</ul>
</section>
<!-- ... -->
</main>
Całość kodu JS umieszczam w funkcji, która uruchomi się po evencie DOMContentLoaded, czyli po załadowaniu kodu HTML. Dzięki temu wiem, że zanim zacznę wyszukiwać elementy, będą one już istnieć w drzewie DOM.
Poniżej umieściłem komentarze, w których po kolei tłumaczę, co dzieje się w kodzie. Pamiętaj jednak, że w projektach komercyjnych zamieszczanie komentarzy nie jest dobrym rozwiązaniem (nazwy funkcji i zmiennych powinny wystarczyć do zrozumienia rozwiązań).
document.addEventListener('DOMContentLoaded', function () {
// wyszukuję listę <ul /> oraz szablon elementu listy
const productsList = document.querySelector('.adminPanel__list');
const productItemTemplate = document.querySelector(
'.adminPanel__item--template'
);
// wyszukuję przycisk dodający produkty
const addBtn = document.querySelector('.adminPanel__addProductBtn');
let productCounter = 1;
// na tym przycisku ustawiam nasłuchiwanie na event 'click'
addBtn.addEventListener('click', function () {
// wykorzystuję szablon elementu listy i usuwam klasę ukrywającą ten element
const newProductItem = productItemTemplate.cloneNode(true);
newProductItem.classList.remove('adminPanel__item--template');
// dodaję treść do nagłówka produktu
const productTitle = newProductItem.querySelector('.product__name');
productTitle.innerText = 'Produkt: ' + productCounter;
// dodaję element listy do rodzica <ul />
productsList.appendChild(newProductItem);
// zwiększam licznik o 1, by nagłówki różniły się liczbą
productCounter++;
});
});
W linii 17 usuwam klasę .adminPanel__item--template, ponieważ w kodzie CSS przypisałem do niej właściwość display z wartością none. Szablon nadal nie będzie widoczny na stronie, a nowe elementy już tak.
Gdyby przełożyć to na nasz przykład z rodziną, to takim nieprawidłowym (a w drugim przypadku – co najmniej nieoptymalnym) podejściem byłaby próba stworzenia sytuacji, w której każdy z członków rodziny mówi sam do siebie „podano obiad” i sam na ten obiad idzie. Jak wiele niepotrzebnie wypowiedzianych słów!
Sposób myślenia w tym przypadku jest taki: mam wiele produktów i wszystkie te produkty mają przycisk „usuń”. Muszę więc wyszukać wszystkie przyciski na stronie, dodać do nich nasłuchiwanie na event click, a w przypisanej do zdarzenia funkcji umieścić kod obsługujący usuwanie produktu.
Rozwiązanie to mogłoby wyglądać tak:
const deleteBtns = document.querySelectorAll('.product__deleteBtn');
for (const btn of deleteBtns) {
btn.addEventListener('click', function () {
const product = btn.closest('.adminPanel__item');
product.remove();
});
}
Sprawdź podgląd. Dlaczego mimo to przycisk „usuń” nie działa? Ponieważ powyższy kod JavaScript skończył się wykonywać po uruchomieniu strony, czyli jeszcze zanim w ogóle pojawiły się na niej nasze produkty z przyciskiem „usuń”. Nie mieliśmy więc szansy „namierzyć ich” – w tym przypadku za pomocą .querySelectorAll() – ponieważ jeszcze nie istniały.
Podobnie stałoby się w kodzie asynchronicznym, gdy próbowalibyśmy wyszukać przyciski zanim nasze produkty pobiorą się z serwera.
Skoro więc problemem jest nieistnienie elementów, to czemu by nie dodać do nich eventListenera w momencie ich tworzenia? Spróbujmy!
Wracamy do kodu, w którym tworzymy nowe elementy po kliknięciu „Dodaj produkt”. Tam do przycisku „usuń” każdego kolejnego produktu dodajemy nasłuchiwanie na click i funkcję kasującą.
let productCounter = 1;
addBtn.addEventListener('click', function () {
const newProductItem = productItemTemplate.cloneNode(true);
newProductItem.classList.remove('adminPanel__item--template');
const productTitle = newProductItem.querySelector('.product__name');
productTitle.innerText = 'Produkt: ' + productCounter;
productsList.appendChild(newProductItem);
productCounter++;
const deleteBtn = newProductItem.querySelector('.product__deleteBtn');
deleteBtn.addEventListener('click', function () {
const product = deleteBtn.closest('.adminPanel__item');
product.remove();
});
});
Nasze rozwiązanie działa (przetestuj)! Ale czy jest optymalne? Wyobraźmy sobie, że na stronie pojawia się kilka tysięcy elementów – to kilka tysięcy funkcji zajmujących pamięć. Problem ten można łatwo
rozwiązać poprzez przeniesienie funkcji anonimowej do osobnej deklaracji funkcji, np. tak:
let productCounter = 1;
addBtn.addEventListener('click', createItem);
function createItem() {
const newProductItem = productItemTemplate.cloneNode(true);
newProductItem.classList.remove('adminPanel__item--template');
const productTitle = newProductItem.querySelector('.product__name');
productTitle.innerText = 'Produkt: ' + productCounter;
productsList.appendChild(newProductItem);
productCounter++;
const deleteBtn = newProductItem.querySelector('.product__deleteBtn');
deleteBtn.addEventListener('click', function () {
const product = deleteBtn.closest('.adminPanel__item');
product.remove();
});
}
Możesz spotkać się z opinią, że delegacja zdarzeń zawsze jest lepsza od powyższego rozwiązania, bo nie zwiększa tak znacznie czasu ładowania strony / nie zajmuje tyle pamięci. Argumenty te jednak pojawiają się w towarzystwie założenia, że mamy do czynienia z dziesiątkami tysięcy elementów na stronie. Możliwe, że nigdy nie spotkasz się z potrzebą wyświetlenia takiej liczby elementów. A jeśli tak, to rozwiążesz to np. przed dodanie paginacji (stron, które zawierają kolejne części listy), filtrowanie zapytań czy lazy loading i problem z wydajnością strony w kontekście nasłuchiwania zdarzeń nie będzie mieć znaczenia.
Jakie rozwiązanie pozwoli nam jednak nie przejmować się powyższym zagadnieniem? Czas na próbę nr 3.
Połączmy teraz przykład stojącej w rzędzie rodziny z naszym panelem administratora. U nas „pradziadkiem” jest element <ul />. To on będzie nasłuchiwał kliknięcia. „Usłyszy”, co prawda, wszystkie kliknięcia wykonane na swoich potomkach, ale zareaguje dopiero wtedy, gdy ktoś kliknie w przycisk „usuń”.
Nazywamy to delegacją zdarzenia.
Dzięki temu zniknie problem wyszukiwania przycisków „usuń” lub dodawania nasłuchiwania bezpośrednio na nich. Dlaczego? Dlatego, że u nas element <ul /> nie jest tworzony dynamicznie. Zostanie więc bez problemu znaleziony na stronie po załadowaniu drzewa DOM (przetestuj).
productsList.addEventListener('click', function (e) {
const clickInitializer = e.target;
const isDeleteBtn =
clickInitializer.classList.contains('product__deleteBtn');
if (isDeleteBtn) {
const product = clickInitializer.closest('.adminPanel__item');
product.remove();
}
});
Zapoznaj się z artykułem „Refaktoryzacja – jak tworzyć krótsze funkcje i czytelniejszy kod”, a następnie spróbuj wykonać refaktoryzację panelu administratora. Sporo kodu można wydzielić do mniejszych funkcji i przy okazji przepraktykować ich odpowiednie nazywanie.
Możesz też poćwiczyć tworzenie klas zgodnie z metodologią BEM, kodując formularz dodawania nowych produktów w pliku index.html. Nie zapomnij wówczas podpiąć na formularzu nasłuchiwania na event submit.
Zapamiętaj powyższe rozwiązanie z propagacją i od teraz używaj go w swoich projektach ( nie musisz jednak używać go zawsze). Internet i aplikacje są pełne eventów oraz dynamicznego tworzenia contentu – czy to z pomocą formularzy, czy korzystania z API. Odtąd więc ustawianie nasłuchiwania na „niedynamicznych przodkach” powinno stać się Twoim chlebem powszednim.
Udostępnij ten artykuł:
Chcesz zostać (lepszym) programistą i lepiej zarabiać?
🚀 Porozmawiajmy o nauce programowania, poszukiwaniu pracy, o rozwoju kariery lub przyszłości branży IT!
Umów się na ✅ bezpłatną i niezobowiązującą rozmowę ze mną.
Chętnie porozmawiam o Twojej przyszłości i pomogę Ci osiągnąć Twoje cele! 🎯