Delegacja i propagacja zdarzeń w JavaScript

1. Propagacja zdarzeń

Propagacja zdarzeń to sposób, w jaki zdarzenia poruszają się w drzewie DOM. Wyróżniamy dwa kierunki propagacji:

  • Faza capturing (przechwytywanie, w dół) – zdarzenie przechodzi od elementu document w dół do elementu, który je wywołał.
  • Faza bubbling (bąbelkowanie, w górę) – zdarzenie przechodzi od elementu, który je wywołał, do góry w drzewie DOM aż do document.

Przykład propagacji zdarzeń

Załóżmy, że mamy następujący HTML:

<div id="parent">
    <button id="child">Click me</button>
</div>

I dodajemy dwa nasłuchiwacze:

document.getElementById("parent").addEventListener("click", () => {
    console.log("PARENT clicked");
});

document.getElementById("child").addEventListener("click", () => {
    console.log("CHILD clicked");
});

Jeśli klikniesz #child, zobaczysz w konsoli:

CHILD clicked!
PARENT clicked!

Dzieje się tak, bo zdarzenie bąbelkuje (przechodzi od dziecka do rodzica).

Zatrzymanie propagacji zdarzeń

Jeśli nie chcemy, aby zdarzenie bąbelkowało w górę, możemy użyć event.stopPropagation():

document.getElementById("child").addEventListener("click", event => {
    event.stopPropagation(); // Zatrzymuje propagację
    console.log("CHILD clicked");
});

Teraz po kliknięciu #child konsola wyświetli tylko: CHILD clicked , #parent nie otrzyma zdarzenia.

Wymuszenie przechwytywania zdarzeń

Domyślnie nasłuchiwanie zdarzeń odbywa się w fazie bąbelkowania, co zonacza, że najpierw wywołane zostanie zdarzenie dla #child , a następnie dla #parent . Można jednak odwrócić tą kolejność, przypisując zdarzenie do do fazy przechwytywania, poprzez przekazanie true jako trzeciego argumentu addEventListener:

document.getElementById("parent").addEventListener("click", () => {
    console.log("PARENT clicked (capturing)");
}, true);

Teraz #parent zostanie uruchomiony przed #child, ponieważ obsługuje zdarzenia w fazie przechwytywania.

2. Delegacja zdarzeń

Delegacja zdarzeń to technika polegająca na przypisywaniu event listenera do elementu nadrzędnego zamiast do każdego potomka osobno. Przydatne, gdy elementy dynamicznie pojawiają się w DOM (np. za pomocą ajax).

document.getElementById("parent").addEventListener("click", event => {
  if (event.target.classList.contains("child")) {
    console.log("Child clicked: ", event.target);
  }
});

Jeśli później dodamy nowy element:

const newChild = document.createElement("div");
newChild.classList.add("child");
newChild.textContent = "Nowy element";
document.getElementById("parent").appendChild(newChild);

Nadal będziemy mogli wykryć kliknięcia w nowy element, mimo że nie dodaliśmy mu osobnego addEventListener.