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);

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ń

Wybór Rodzica do Delegowania

Wybierając element nadrzędny (rodzicSelektor) do delegowania, staraj się wybrać element, który:

  1. Jest jak najbliżej elementów docelowych (potomekSelektor) w drzewie DOM, aby zminimalizować drogę bąbelkowania.
  2. 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ń:

  1. 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).
  2. Dołącz jeden delegowany handler zdarzenia "click" do elementu <tbody> tabeli, który będzie nasłuchiwał kliknięć na przyciski z klasą .usun-przycisk.
  3. 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ń:

  1. Po kliknięciu "Dodaj Element", dodaj nowy element <li>Nowy element <span class="numer">X</span></li> do listy, gdzie X to kolejny numer.
  2. Dołącz delegowany handler zdarzenia mouseenter do listy #dynamiczna-lista, który będzie nasłuchiwał najechania na elementy span.numer wewnątrz listy.
  3. Wewnątrz handlera mouseenter, zmień kolor tła najechanego elementu span.numer na żółty.
  4. Dołącz drugi delegowany handler zdarzenia mouseleave do listy, który przywróci domyślne tło elementu span.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).