Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!
⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do Discorda!
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ł:
Potrzebujesz cotygodniowej dawki motywacji?
Zapisz się i zgarnij za darmo e-book o wartości 39 zł!
PS. Zazwyczaj rozsyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu email.
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! 🎯