Moduł7 - Zajęcia 13 - Delegacja zdarzeń
Propagacja zdarzenia
Propagacja zdarzenia (event propagation) to termin opisujący cykl życia zdarzenia, który obejmuje trzy etapy: przechwytywanie, celowanie i bąbelkowanie (bubbling phase). W praktyce najczęściej wykorzystywana jest tylko faza bąbelkowania.
Kiedy wydarzenie ma miejsce, przechodzi przez trzy obowiązkowe fazy:
- Capturing phase (przechwytywanie) - zdarzenie rozpoczyna swój cykl życia w window i "opada" (przechodzi przez wszystkie elementy przodków) do najgłębszego elementu docelowego, na którym nastąpiła akcja, np. kliknięcie.
- Target phase (celowanie) - zdarzenie dotarło do elementu docelowego. Ten etap obejmuje tylko powiadomienie elementu, że dana akcja została na nim wykonana.
- Bubbling phase (bąbelkowanie) - ostatnia faza, w której zdarzenie bąbelkuje od najgłębszego, docelowego elementu, przez wszystkie elementy przodków, aż do window.
Propagacja jest często niewłaściwie używana jako synonim etapu bąbelkowania. Różnica między tymi pojęciami polega na tym, że propagację możemy kontrolować na poziomie kodu, ale bąbęlkowanie zawsze odbywa się z dołu (innermost element) do góry (outermost element).
Bąbelkowanie (bulgotanie) zdarzenia
Gdy wystąpi zdarzenie, procedury obsługi są najpierw wywoływane na najgłębiej zagnieżdżonym elemencie, następnie na jego rodzicu, następnie na przodu o poziom wyżej i tak dalej, w górę łańcucha zagnieżdżenia. Proces ten nazywa się bąbelkowaniem (event bubbling), ponieważ zdarzenia podobnie jak bąbelki powietrza idą w górę, od wewnętrznego elementu, przez wszystkich przodków aż do window
Spójrzmy na przykład, aby było bardziej zrozumiałe. Weźmy trzy zagnieżdżone znaczniki "div" z obsługą kliknięcia na każdym z nich.
Bąbelkowanie sprawia, że kliknięcie na element #descendant wywoła obsługę kliknięcia, jeśli taka istnieje, najpierw na samym #descendant, potem na elemencie #child, potem na elemencie #parent i tak dalej w górę łańcucha przodków aż do window
Dlatego, jeśli w powyższym przykładzie klikniemy na #descendant, zostanie sekwencyjnie wyświetlony alert dla descendant → child → parent.
Przykład -------------------------------
JavaScript:
const parent = document.querySelector('#section2-parent');
const child = document.querySelector('#section2-child');
const descendant = document.querySelector('#section2-descendant');
let parentCount = 0;
let childCount = 0;
let descendantCount = 0;
parent.addEventListener('click', () => {
parentCount++;
parent.firstChild.textContent = `PARENT kliknięcie ${parentCount}`;
});
child.addEventListener('click', () => {
childCount++;
child.firstChild.textContent = `CHILD kliknięcie ${childCount}`;
});
descendant.addEventListener('click', () => {
descendantCount++;
descendant.firstChild.textContent = `DESCENDANT kliknięcie ${descendantCount}`;
});
PrzykładEND -------------------------------
Bąbelkują prawie wszystkie zdarzenia, natomiast zdarzenia focus i blur nie bąbelkują, więc istnieją ich "bąbelkujące" odpowiedniki - focusin i focusout.
Właściwość event.target
Niezależnie od tego, gdzie złapaliśmy zdarzenie podczas jego bąbelkowania, zawsze możesz dowiedzieć się, gdzie dokładnie nastąpiło zdarzenie. Najgłębszy element, który wywołuje zdarzenie, nazywa się elementem docelowym lub źródłowym i jest dostępny jako event.target.
- event.target to odwołanie do elementu "źródłowego", na którym wystąpiło zdarzenie, pozostaje więc niezmienne podczas procesu bąbelkowania.
- event.currentTarget to odwołanie do bieżącego elementu, do którego dotarło bąbelkowanie, a więc tego na którym aktualnie działa procedura obsługi zdarzeń.
Jeśli detektor zdarzeń jest zarejestrowany na najwyższym elemencie, "przechwyci" wszystkie kliknięcia wewnątrz, ponieważ zdarzenia będą bąbelkować do tego elementu. Otwórz konsolę w przykładzie i kliknij, event.target jest zawsze oryginalnym (i najgłębszym) klikniętym elementem, a event.currentTarget zmienia się.
Przykład -------------------------------
JavaScript:
const parent_s1a2 = document.querySelector('#section2a2-parent');
const list_s1a2 = document.querySelector('#section2a2-list');
parent_s1a2.addEventListener('click', event => {
const eventTargetlistItem = document.createElement('li');
eventTargetlistItem.classList.add('item');
eventTargetlistItem.textContent =
`event.target: ${event.target.firstChild.textContent}
event.currentTarget: ${event.currentTarget.firstChild.textContent}`;
list_s1a2.append(eventTargetlistItem);
});
PrzykładEND -------------------------------
Zatrzymanie bąbelkowania
Zazwyczaj zdarzenie będzie bąbelkować do góry do elementu window, wywołując wszelkie procedury obsługi na swojej drodze. Każda pośrednia funkcja obsługi może zdecydować, że zdarzenie zostało już całkowicie obsłużone i zatrzymać propagację poprzez wywołanie metody stopPropagation().
Przykład -------------------------------
JavaScript:
const parent_s2a3 = document.querySelector('#section2a3-parent');
const child_s2a3 = document.querySelector('#section2a3-child');
const descendant_s2a3 = document.querySelector('#section2a3-descendant');
parent_s2a3.addEventListener('click', () => {
alert(
'Parent click handler. This alert will not appear when clicking on Descendant,
the event will not reach here!'
);
});
child_s2a3.addEventListener('click', () => {
alert(
'Child click handler. This alert will not appear when clicking on Descendant,
the event will not reach here!'
);
});
descendant_s2a3.addEventListener('click', event => {
event.stopPropagation();
alert('Descendant click handler');
});
PrzykładEND -------------------------------
Jeśli element ma kilka procedur obsługi dla jednego zdarzenia, to nawet przy zatrzymaniu propagacji wszystkie zostaną wykonane. Oznacza to, że metoda stopPropagation() zapobiega jedynie dalszemu postępowi zdarzenia na poziom wyżej.
Jeśli chcesz całkowicie zatrzymać przetwarzanie zdarzenia w "równoległych" procedurach , użyj metody stopImmediatePropagation(). Zapobiegnie ona nie tylko bąbelkowaniu, ale także zatrzyma przetwarzanie zdarzeń w bieżącym elemencie.
Nie zatrzymuj bąbelkowania bez potrzeby. Zatrzymanie bąbelkowania tworzy własne pułapki, które należy następnie ominąć. Na przykład narzędzia analityczne używają bąbelkowania do śledzenia zdarzeń na stronie.
Delegowanie zdarzeń
Bąbelkowanie pozwala na zaimplementowanie jednego z najbardziej przydatnych podejść - delegowania zdarzeń (event delegation). Polega ono na tym, że jeśli istnieje grupa elementów, których zdarzenia muszą być przetwarzane w ten sam sposób, to do ich wspólnego przodka dodawany jest jeden handler, zamiast dodawania handlera do każdego elementu. Używając właściwości event.target, możesz uzyskać odwołanie do elementu docelowego, dowiedzieć się, na którym potomku wystąpiło zdarzenie i odpowiednio je obsłużyć.
Spójrzmy na delegację na przykładzie. Utwórzmy element <div>, dodajmy do niego dowolną liczbę przycisków, na przykład 100, i zarejestrujmy każdy z nich za pomocą detektora zdarzeń kliknięcia z funkcją handleButtonClick.
Problem polega na tym, że mamy 100 słuchaczy zdarzeń. Wszystkie wskazują na tę samą funkcję, ale samych handlerów jest 100. Co jeśli przeniesiemy wszystkich słuchaczy do wspólnego przodka, elementu <div>?
Teraz jest tylko jeden handler do obsługi zdarzeń kliknięcia, a przeglądarka nie musi przechowywać w pamięci 100 różnych słuchaczy. Delegowanie sprowadza się więc do trzech prostych kroków.
- Określ wspólnego przodka grupy elementów śledzenia zdarzeń.
- Zarejestruj na elemencie nadrzędnym procedurę obsługi zdarzeń, którą chcemy przechwycić z grupy elementów.
- W procedurze obsługi użyj event.target, aby wybrać element docelowy.
Takie podejście upraszcza inicjowanie słuchaczy elementów tego samego typu. Możesz dodawać, usuwać lub modyfikować elementy bez ręcznego dodawania lub usuwania procedur obsługi zdarzeń.
Paleta kolorów
Stworzymy paletę kolorów z możliwością wyboru koloru poprzez kliknięcie i wyświetlenie wybranego koloru. Zamiast przypisywać handler do każdego elementu palety, których może być bardzo dużo, "doczepimy" jednego słuchacza na wspólnym przodku div.color-palette. W obsłudze zdarzenia kliknięcia używamy event.target, aby pobrać element, na którym wystąpiło zdarzenie i powiązany z nim kolor, który będziemy przechowywać w atrybucie data-color.
Przykład -------------------------------
PrzykładEND -------------------------------
Pamiętaj, aby sprawdzić cel kliknięcia, żeby zdecydowanie był to przycisk, nie chcemy przetwarzać kliknięć w element kontenera. Aby sprawdzić typ elementu, użyj właściwości nodeName.