Delegování události

Zachycování a probublávání nám umožňují implementovat jeden z nejúčinnějších vzorů zpracování událostí zvaný delegování událostí .

Myšlenka je taková, že pokud máme mnoho prvků ošetřených podobným způsobem, pak namísto přiřazení handleru každému z nich nasadíme jeden handler na jejich společného předka.

V handleru dostaneme event.target vidět, kde se událost skutečně stala, a zvládnout ji.

Podívejme se na příklad – diagram Ba-Gua odrážející starou čínskou filozofii.

Tady to je:

HTML je takto:

<table>
 <tr>
 <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
 </tr>
 <tr>
 <td class="nw"><strong>Northwest</strong><br>Metal<br>Silver<br>Elders</td>
 <td class="n">...</td>
 <td class="ne">...</td>
 </tr>
 <tr>...2 more lines of this kind...</tr>
 <tr>...2 more lines of this kind...</tr>
</table>

Tabulka má 9 buněk, ale může jich být 99 nebo 9999, na tom nezáleží.

Naším úkolem je zvýraznit buňku <td> při kliknutí.

Místo přiřazení onclick handler ke každému <td> (může být mnoho) – nastavíme obslužnou rutinu „catch-all“ na <table> prvek.

Bude používat event.target získat prvek, na který kliknete, a zvýraznit jej.

Kód:

let selectedTd;

table.onclick = function(event) {
 let target = event.target; // where was the click?

 if (target.tagName != 'TD') return; // not on TD? Then we're not interested

 highlight(target); // highlight it
};

function highlight(td) {
 if (selectedTd) { // remove the existing highlight if any
 selectedTd.classList.remove('highlight');
 }
 selectedTd = td;
 selectedTd.classList.add('highlight'); // highlight the new td
}

Takový kód se nestará o to, kolik buněk v tabulce je. Můžeme přidat/odebrat <td> dynamicky kdykoli a zvýraznění bude stále fungovat.

Přesto to má jednu nevýhodu.

Ke kliknutí nemusí dojít na <td> , ale uvnitř.

V našem případě, když se podíváme do HTML, můžeme vidět vnořené značky uvnitř <td> , například <strong> :

<td>
 <strong>Northwest</strong>
 ...
</td>

Samozřejmě, pokud dojde ke kliknutí na <strong> pak se stane hodnotou event.target .

V obslužné rutině table.onclick měli bychom vzít takový event.target a zjistěte, zda bylo kliknutí uvnitř <td> nebo ne.

Zde je vylepšený kód:

table.onclick = function(event) {
 let td = event.target.closest('td'); // (1)

 if (!td) return; // (2)

 if (!table.contains(td)) return; // (3)

 highlight(td); // (4)
};

Vysvětlení:

  1. Metoda elem.closest(selector) vrátí nejbližšího předka, který odpovídá selektoru. V našem případě hledáme <td> na cestě nahoru od zdrojového prvku.
  2. Pokud event.target není v žádném <td> , pak se hovor okamžitě vrátí, protože není co dělat.
  3. V případě vnořených tabulek event.target může být <td> , ale ležící mimo aktuální tabulku. Zkontrolujeme, zda je to skutečně náš stůl <td> .
  4. A pokud ano, zvýrazněte to.

Výsledkem je rychlý a efektivní zvýrazňovací kód, který se nestará o celkový počet <td> v tabulce.

Příklad delegování:akce v označení

Delegování událostí má i další využití.

Řekněme, že chceme vytvořit nabídku s tlačítky „Uložit“, „Načíst“, „Vyhledat“ a tak dále. A je tu objekt s metodami save , load , search … Jak je spárovat?

První nápad může být přiřadit každému tlačítku samostatný handler. Ale existuje elegantnější řešení. Můžeme přidat handler pro celé menu a data-action atributy pro tlačítka, která mají metodu k volání:

<button data-action="save">Click to Save</button>

Obslužná rutina přečte atribut a provede metodu. Podívejte se na pracovní příklad:

<div id="menu">
 <button data-action="save">Save</button>
 <button data-action="load">Load</button>
 <button data-action="search">Search</button>
</div>

<script>
 class Menu {
 constructor(elem) {
 this._elem = elem;
 elem.onclick = this.onClick.bind(this); // (*)
 }

 save() {
 alert('saving');
 }

 load() {
 alert('loading');
 }

 search() {
 alert('searching');
 }

 onClick(event) {
 let action = event.target.dataset.action;
 if (action) {
 this[action]();
 }
 };
 }

 new Menu(menu);
</script>

Vezměte prosím na vědomí, že this.onClick je vázán na this v (*) . To je důležité, protože jinak this uvnitř by odkazoval na prvek DOM (elem ), nikoli Menu objekt a this[action] nebylo by to, co potřebujeme.

Jaké výhody nám zde delegování přináší?

  • Nemusíme psát kód, abychom každému tlačítku přiřadili obslužnou rutinu. Jednoduše vytvořte metodu a vložte ji do označení.
  • Struktura HTML je flexibilní, tlačítka můžeme kdykoli přidat/odebrat.

Můžeme také použít třídy .action-save , .action-load , ale atribut data-action je sémanticky lepší. A můžeme to použít i v pravidlech CSS.

Vzorec „chování“

Můžeme také použít delegování událostí k přidání „chování“ k prvkům deklarativně , se speciálními atributy a třídami.

Vzor má dvě části:

  1. K prvku přidáváme vlastní atribut, který popisuje jeho chování.
  2. Obslužný program pro celý dokument sleduje události, a pokud k události dojde u přiřazeného prvku, provede akci.

Chování:Počítadlo

Například zde atribut data-counter přidává chování:„zvýšit hodnotu při kliknutí“ k tlačítkům:

Counter: <input type="button" value="1" data-counter>
One more counter: <input type="button" value="2" data-counter>

<script>
 document.addEventListener('click', function(event) {

 if (event.target.dataset.counter != undefined) { // if the attribute exists...
 event.target.value++;
 }

 });
</script>

Pokud klikneme na tlačítko – jeho hodnota se zvýší. Ne tlačítka, ale obecný přístup je zde důležitý.

S data-counter může být tolik atributů jak chceme. Do HTML můžeme kdykoli přidat nové. Pomocí delegování události jsme „rozšířili“ HTML, přidali atribut, který popisuje nové chování.

Pro ovladače na úrovni dokumentů – vždy addEventListener

Když k document přiřadíme obsluhu události objekt, měli bychom vždy používat addEventListener , nikoli document.on<event> , protože to druhé způsobí konflikty:nové handlery přepisují staré.

U skutečných projektů je normální, že na document je mnoho handlerů nastavit různými částmi kódu.

Chování:Přepínač

Ještě jeden příklad chování. Kliknutí na prvek s atributem data-toggle-id zobrazí/skryje prvek s daným id :

<button data-toggle-id="subscribe-mail">
 Show the subscription form
</button>

<form id="subscribe-mail" hidden>
 Your mail: <input type="email">
</form>

<script>
 document.addEventListener('click', function(event) {
 let id = event.target.dataset.toggleId;
 if (!id) return;

 let elem = document.getElementById(id);

 elem.hidden = !elem.hidden;
 });
</script>

Všimněme si ještě jednou, co jsme udělali. Chcete-li nyní prvku přidat funkci přepínání – není potřeba znát JavaScript, stačí použít atribut data-toggle-id .

To může být opravdu pohodlné – není třeba psát JavaScript pro každý takový prvek. Stačí použít chování. Obslužný program na úrovni dokumentu umožňuje, aby fungoval pro jakýkoli prvek stránky.

Můžeme také kombinovat více chování na jednom prvku.

Vzor „chování“ může být alternativou k mini-fragmentům JavaScriptu.

Shrnutí

Delegace na akci je opravdu skvělá! Je to jeden z nejužitečnějších vzorů pro události DOM.

Často se používá k přidání stejného zpracování pro mnoho podobných prvků, ale nejen pro to.

Algoritmus:

  1. Na kontejner umístěte jeden ovladač.
  2. V obslužné rutině – zkontrolujte zdrojový prvek event.target .
  3. Pokud se událost stala uvnitř prvku, který nás zajímá, pak událost zpracujte.

Výhody:

  • Zjednodušuje inicializaci a šetří paměť:není třeba přidávat mnoho obslužných programů.
  • Méně kódu:při přidávání nebo odebírání prvků není třeba přidávat/odebírat obslužné nástroje.
  • Úpravy DOM:můžeme hromadně přidávat/odebírat prvky pomocí innerHTML a podobně.

Delegování má samozřejmě svá omezení:

  • Za prvé, událost musí být bublající. Některé akce neprobublávají. Nízkoúrovňové manipulátory by také neměly používat event.stopPropagation() .
  • Zadruhé, delegování může zvýšit zátěž CPU, protože obslužná rutina na úrovni kontejneru reaguje na události na kterémkoli místě kontejneru, bez ohledu na to, zda nás zajímají nebo ne. Ale obvykle je zatížení zanedbatelné, takže to nebereme v úvahu.