Lekcja 8: Zdarzenia w jQuery - Delegowanie Zdarzeń
W poprzedniej lekcji nauczyliśmy się dołączać handlery zdarzeń bezpośrednio do istniejących elementów za pomocą .on()
. Jednak co w sytuacji, gdy elementy są dodawane do strony dynamicznie (np. przez AJAX lub inny kod jQuery) już po załadowaniu strony i podpięciu handlerów? Bezpośrednio dołączone handlery nie będą działać na tych nowych elementach. Rozwiązaniem tego problemu jest delegowanie zdarzeń.
Problem z Dynamicznie Dodawanymi Elementami
Rozważmy przykład:
<ul id="lista">
<li>Punkt 1</li>
<li>Punkt 2</li>
</ul>
<button id="dodaj">Dodaj Punkt</button>
<script>
$(function() {
// Dołącz handler do istniejących elementów li
$("#lista li").on("click", function() {
$(this).toggleClass("zaznaczony");
});
// Dodaj nowy element li po kliknięciu przycisku
$("#dodaj").on("click", function() {
$("#lista").append("<li>Nowy Punkt</li>");
});
});
</script>
W powyższym kodzie, kliknięcie na "Punkt 1" i "Punkt 2" będzie przełączać klasę "zaznaczony". Jednak kliknięcie na "Nowy Punkt", dodany później przez przycisk, nie wywoła handlera, ponieważ w momencie jego dołączania ($("#lista li").on(...)
) ten nowy element jeszcze nie istniał.
Rozwiązanie: Delegowanie Zdarzeń za pomocą .on()
Delegowanie zdarzeń polega na dołączeniu handlera nie do samych elementów docelowych (które mogą jeszcze nie istnieć), ale do ich stabilnego elementu nadrzędnego (rodzica lub przodka), który istnieje już w momencie dołączania handlera. Wykorzystujemy specjalną składnię metody .on()
:
$(rodzicSelektor).on(nazwaZdarzenia, potomekSelektor, [dane], funkcjaObslugujaca);
rodzicSelektor
: Selektor dla stabilnego elementu nadrzędnego, który będzie "nasłuchiwał" zdarzeń od swoich potomków.nazwaZdarzenia
: Typ zdarzenia (np.\\'click\\'
).potomekSelektor
: Selektor dla elementów potomnych, na których faktycznie chcemy obsłużyć zdarzenie. Handler zostanie wywołany tylko wtedy, gdy zdarzenie wystąpi na elemencie pasującym do tego selektora (lub jego potomku) i "przebąbelkuje" do rodzica.[dane]
(opcjonalne): Dane przekazywane do handlera.funkcjaObslugujaca
: Funkcja, która zostanie wykonana. Wewnątrz tej funkcjithis
(orazevent.currentTarget
) będzie odnosić się do elementu pasującego dopotomekSelektor
, a nie do rodzica.
Poprawiony Przykład z Delegowaniem
<ul id="lista">
<li>Punkt 1</li>
<li>Punkt 2</li>
</ul>
<button id="dodaj">Dodaj Punkt</button>
<script>
$(function() {
// Dołącz handler do rodzica (#lista), delegując obsługę do potomków (li)
$("#lista").on("click", "li", function() {
// \'this\' odnosi się teraz do klikniętego elementu \'li\'
$(this).toggleClass("zaznaczony");
console.log("Kliknięto:", $(this).text());
});
// Dodaj nowy element li po kliknięciu przycisku
$("#dodaj").on("click", function() {
$("#lista").append("<li>Nowy Punkt</li>");
});
});
</script>
Teraz, mimo że handler jest dołączony do #lista
, będzie on poprawnie obsługiwał kliknięcia zarówno na istniejących, jak i na dynamicznie dodanych elementach li
. Dzieje się tak, ponieważ zdarzenie "click" na li
bąbelkuje w górę do #lista
. Handler na #lista
sprawdza, czy element, który pierwotnie wywołał zdarzenie (event.target
), pasuje do selektora potomka ("li"
). Jeśli tak, funkcja obsługująca jest wykonywana w kontekście tego potomka (this
wskazuje na li
).
Zalety Delegowania Zdarzeń
- Obsługa dynamicznie dodawanych elementów: Główna zaleta, jak pokazano powyżej.
- Wydajność: Zamiast dołączać potencjalnie setki handlerów do pojedynczych elementów (np. w dużej tabeli), dołączamy tylko jeden handler do wspólnego rodzica. Zmniejsza to zużycie pamięci i może przyspieszyć inicjalizację strony.
- Prostszy kod: Nie trzeba pamiętać o ponownym dołączaniu handlerów po każdej dynamicznej zmianie w DOM.
Wybór Rodzica do Delegowania
Wybierając element nadrzędny (rodzicSelektor
) do delegowania, staraj się wybrać element, który:
- Jest jak najbliżej elementów docelowych (
potomekSelektor
) w drzewie DOM, aby zminimalizować drogę bąbelkowania. - Jest stabilny, tzn. sam nie jest dynamicznie dodawany i usuwany.
Często dobrym wyborem jest bezpośredni rodzic dynamicznie dodawanych elementów lub kontener, w którym się znajdują. Unikaj delegowania od zbyt ogólnych elementów jak document
czy body
, jeśli nie jest to absolutnie konieczne, ponieważ będą one musiały przetwarzać znacznie więcej niepotrzebnych zdarzeń.
Usuwanie Delegowanych Handlerów
Delegowane handlery usuwa się również za pomocą metody .off()
, podając te same argumenty (w tym selektor potomka), które zostały użyte przy ich tworzeniu:
// Usunięcie konkretnego delegowanego handlera
$("#lista").off("click", "li", nazwanaFunkcjaObslugujaca);
// Usunięcie wszystkich delegowanych handlerów "click" dla potomków "li"
$("#lista").off("click", "li");
// Usunięcie wszystkich delegowanych handlerów dołączonych do #lista
// (nie wpłynie na handlery dołączone bezpośrednio do #lista)
// Uwaga: składnia .off() z selektorem potomka jest specyficzna dla usuwania delegowanych handlerów.
// Aby usunąć WSZYSTKIE handlery (bezpośrednie i delegowane) z #lista, użyj po prostu $("#lista").off();
Zadanie praktyczne
Stwórz plik HTML z pustą tabelą <table id="tabela-danych" border="1"><thead><tr><th>ID</th><th>Nazwa</th><th>Akcje</th></tr></thead><tbody></tbody></table>
oraz przyciskiem <button id="dodaj-wiersz">Dodaj Wiersz</button>
.
Używając jQuery i delegowania zdarzeń:
- Po kliknięciu przycisku "Dodaj Wiersz", dodaj nowy wiersz do
<tbody>
tabeli. Wiersz powinien zawierać przykładowe dane i przycisk "Usuń" w ostatniej komórce, np.:<tr><td>1</td><td>Produkt A</td><td><button class="usun-przycisk">Usuń</button></td></tr>
(zmieniaj ID i nazwę dla kolejnych wierszy). - Dołącz jeden delegowany handler zdarzenia "click" do elementu
<tbody>
tabeli, który będzie nasłuchiwał kliknięć na przyciski z klasą.usun-przycisk
. - Wewnątrz tego handlera, po kliknięciu przycisku "Usuń", usuń cały wiersz tabeli (
<tr>
), w którym znajduje się kliknięty przycisk. (Wskazówka: użyj$(this).closest(\'tr\').remove();
).
Pokaż rozwiązanie
HTML:
<!DOCTYPE html>
<html>
<head>
<title>Test Delegowania Zdarzeń</title>
</head>
<body>
<button id="dodaj-wiersz">Dodaj Wiersz</button>
<table id="tabela-danych" border="1">
<thead>
<tr>
<th>ID</th>
<th>Nazwa</th>
<th>Akcje</th>
</tr>
</thead>
<tbody>
<!-- Wiersze będą dodawane dynamicznie -->
</tbody>
</table>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<script>
$(function() {
let licznikWierszy = 0;
// 1. Dodawanie nowego wiersza
$("#dodaj-wiersz").on("click", function() {
licznikWierszy++;
let nowyWiersz = `
<tr>
<td>${licznikWierszy}</td>
<td>Produkt ${String.fromCharCode(64 + licznikWierszy)}</td> <!-- Produkt A, B, C... -->
<td><button class="usun-przycisk">Usuń</button></td>
</tr>`;
$("#tabela-danych tbody").append(nowyWiersz);
console.log("Dodano wiersz ID:", licznikWierszy);
});
// 2. i 3. Delegowany handler dla przycisków "Usuń"
$("#tabela-danych tbody").on("click", ".usun-przycisk", function() {
// \'this\' odnosi się do klikniętego przycisku .usun-przycisk
let wierszDoUsuniecia = $(this).closest(\'tr\');
let idWiersza = wierszDoUsuniecia.find(\'td:first\').text(); // Pobierz ID z pierwszej komórki
wierszDoUsuniecia.remove();
console.log("Usunięto wiersz ID:", idWiersza);
});
});
</script>
</body>
</html>
Zadanie do samodzielnego wykonania
Stwórz listę <ul id="dynamiczna-lista"></ul>
i przycisk "Dodaj Element".
Używając jQuery i delegowania zdarzeń:
- Po kliknięciu "Dodaj Element", dodaj nowy element
<li>Nowy element <span class="numer">X</span></li>
do listy, gdzie X to kolejny numer. - Dołącz delegowany handler zdarzenia
mouseenter
do listy#dynamiczna-lista
, który będzie nasłuchiwał najechania na elementyspan.numer
wewnątrz listy. - Wewnątrz handlera
mouseenter
, zmień kolor tła najechanego elementuspan.numer
na żółty. - Dołącz drugi delegowany handler zdarzenia
mouseleave
do listy, który przywróci domyślne tło elementuspan.numer
po zjechaniu z niego myszką.
FAQ - Delegowanie Zdarzeń w jQuery
Kiedy powinienem używać delegowania zdarzeń?
Głównie wtedy, gdy elementy, na których chcesz obsłużyć zdarzenie, są dodawane do strony dynamicznie po jej załadowaniu. Jest to również korzystne ze względów wydajnościowych, gdy masz bardzo dużo elementów, do których musiałbyś dołączyć ten sam handler (np. komórki w dużej tabeli, elementy listy).
Czy delegowanie działa dla wszystkich typów zdarzeń?
Delegowanie opiera się na mechanizmie bąbelkowania zdarzeń (event bubbling). Działa dla większości popularnych zdarzeń, takich jak click
, mousedown
, mouseup
, keydown
, keyup
, keypress
, change
. Jednak niektóre zdarzenia, jak focus
, blur
, mouseenter
, mouseleave
, load
, unload
, error
, domyślnie nie bąbelkują i delegowanie ich w standardowy sposób może nie działać zgodnie z oczekiwaniami lub wymagać specjalnych technik (jQuery często radzi sobie z tym wewnętrznie dla focus
/blur
, ale warto być świadomym).
Jaki element wybrać jako "rodzica" do delegowania?
Najlepiej wybrać najbliższego, stabilnego przodka elementów docelowych. Im bliżej, tym mniej zdarzeń musi być przetworzonych niepotrzebnie. Unikaj delegowania od document
lub body
, chyba że nie ma innej sensownej opcji (np. dla modali dodawanych bezpośrednio do body).
Czy this
w delegowanym handlerze odnosi się do rodzica czy potomka?
W delegowanym handlerze zdefiniowanym jako $(rodzic).on(typ, potomek, funkcja)
, słowo kluczowe this
(oraz event.currentTarget
) wewnątrz funkcja
odnosi się do elementu pasującego do selektora potomek
, który wywołał zdarzenie, a nie do rodzic
.
Czy mogę używać metod skróconych (np. .click()
) do delegowania?
Nie, metody skrócone jak .click(handler)
są równoważne .on("click", handler)
i nie obsługują składni delegowania z selektorem potomka. Do delegowania zawsze należy używać pełnej składni .on(typ, selektorPotomka, handler)
.