Capturing e bubbling ci permettono di implementare uno dei pattern più potenti nella gestione degli eventi, e cioè event delegation.
Il concetto di base è che se abbiamo una serie di elementi gestiti in maniera simile, allora, invece di assegnare un gestore per ognuno di essi, possiamo metterne uno solo sui loro antenati comuni.
Nel gestore avremo a disposizione event.target per controllare lâelemento dal quale è partito lâevento e poterlo quindi gestire di conseguenza.
Guardiamo un esempio â il diagramma Ba-Gua che riflette lâantica filosofia Cinese.
Eccola qui:
Questo è lâHTML:
<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>
La tabella è composta da 9 celle, ma potrebbero anche essercene 99 o 9999, è irrilevante.
Il nostro compito è quello di evidenziare un cella <td> al click.
Invece di assegnare un gestore allâonclick per ogni <td> (potrebbero essercene tantissime) â andiamo a impostare un gestore che sarà in grado di âcatturali tuttiâ sullâelemento <table>.
Verrà utilizzato event.target per ottenere lâelemento cliccato ed evidenziarlo.
Ecco il codice:
let selectedTd;
table.onclick = function(event) {
let target = event.target; // dove e' stato il click?
if (target.tagName != 'TD') return; // non era un TD? Allora non siamo interessati
highlight(target); // evidenzialo
};
function highlight(td) {
if (selectedTd) { // rimuove l'evidenziazione esistente, se presente
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // evidenzia il nuovo td
}
Con un codice del genere non importa quante celle ci sono nella tabella. Possiamo aggiungere e rimuovere <td> dinamicamente in qualunque momento e lâevidenziazione continuerà a funzionare.
Ma abbiamo ancora un inconveniente.
Il click potrebbe avvenire non sul <td>, ma in un elemento interno.
Nel nostro cosa se osserviamo dentro lâHTML, possiamo vedere dei tags annidati dentro il <td>, come ad esempio <strong>:
<td>
<strong>Northwest</strong>
...
</td>
Naturalmente, se cliccassimo su questo <strong> proprio questo sarebbe il valore assunto da event.target.
Nel gestore table.onclick, dovremmo perndere questo event.target e scoprire se il click sia avvenuto dentro il <td> oppure no.
Ecco il codice migliorato:
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
if (!td) return; // (2)
if (!table.contains(td)) return; // (3)
highlight(td); // (4)
};
Chiarimenti:
- Il metodo
elem.closest(selector)ritorna lâantenato più vicino che combacia con il selettore. Nel nostro caso cerchiamo un<td>verso lâalto dallâelemento di origine dellâevento. - Se
event.targetnon è dentro nessun<td>, la chiamata esce immediatamente, dal momento che non câè nulla da fare. - Ne caso di tabelle annidate,
event.targetpotrebbe riferirsi ad<td>, ma fuori dalla tabelle corrente. Quindi andiamo a controllare se<td>appartiene alla nostra tabella. - E se così, la evidenziamo.
Come risultato, averemo un codice di evidenziazione veloce ed efficiente, indipendente dal numero di <td> nella tabella.
Esempio di delegation: azioni nel markup
Esistono altri utilizzi per lâevent delegation.
Poniamo il caso che volessimo fare un menù con i pulsanti âSaveâ, âLoadâ, âSearchâ e cosi via, e che vi sia un oggetto con i metodi save, load, search⦠Come potremmo distinguerli?
La prima idea potrebbe essere quella di assegnare dei gestori separati per ogni pulsante. Esiste però una soluzione più elegante. Possiamo aggiungere un gestore per lâintero menù e degli attributi data-action per i pulsanti che devono chiamare il metodo:
<button data-action="save">Clicca per salvare</button>
Il gestore legge lâattributo ed esegue il metodo. Diamo uno sguardo allâesempio:
<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>
Nota bene che this.onClick è collegato a this nel punto (*). Questo è importante, altrimenti this si riferirebbe allâelemento del DOM (elem), e non lâoggetto Menu, di conseguenza this[action] non farebbe quello di cui abbiamo bisogno.
Quindi, quali vantaggi apporta delegation qui?
- Non abbiamo bisogno di scrivere del codice per assegnare un gestore ad ogni pulsante. Ma solo di un metodo e porlo dentro il markup.
- La struttura HTML è flessibile, possiamo aggiungere e rimuovere pulsanti in ogni momento.
Possiamo anche usare classi come .action-save, .action-load, ma un attributo data-action è semanticamente migliore. Inoltre possiamo usarlo nelle regole CSS.
Il pattern âcomportamentaleâ
Possiamo anche usare la event delegation per aggiungere âcomportamentiâ agli elementi in modo dichiarativo, con speciali attributi e classi.
Il pattern consta di due parti:
- Aggiungiamo un attributo personalizzato a un elemento, che descrive il suo comportamento.
- Un gestore su tutto il documento tiene traccia degli eventi, e se viene attivato un evento su un elemento con quellâattributo â esegue lâazione.
Comportamento: contatore
Per esempio, qui lâattributo data-counter aggiunge un comportamento: âincrementa il valore al clickâ sui pulsanti:
Counter: <input type="button" value="1" data-counter>
Contatore di addizione: <input type="button" value="2" data-counter>
<script>
document.addEventListener('click', function(event) {
if (event.target.dataset.counter != undefined) { // se esiste l'attributo...
event.target.value++;
}
});
</script>
Se clicchiamo un pulsante â il suo valore aumenterà . Non sono importanti i pulsanti qui, ma lâapproccio in generale.
Possono esserci quanti attributi data-counter vogliamo. Possiamo aggiungerne di nuovi allâHTML in ogni momento. Usando la event delegation abbiamo âestesoâ HTML, aggiunto un attributo che descrive un nuovo comportamento.
addEventListenerQuando assegniamo un gestore di eventi allâoggetto document, dovremmo sempre usare addEventListener, e non document.on<event>, perché il secondo causerebbe conflitti: i nuovi gestori sovrascriverebbero i precedenti.
Per progetti reali è normale che vi siano molti gestori su document impostati in punti differenti del codice.
Comportamento: toggler
Ancora un esempio di comportamento. Un click su un elemento con lâattributo data-toggle-id mostrerà o nasconderà lâelemento con il dato id:
<button data-toggle-id="subscribe-mail">
Mostra il form di sottoscrizione
</button>
<form id="subscribe-mail" hidden>
La tua 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>
Notiamo ancora una volta cosa abbiamo fatto. Adesso, per aggiungere la funzionalità di toggling su un elemento â non è necessario conoscere JavaScript, è sufficiente usare lâattributo data-toggle-id.
Questo può essere davvero conveniente â nessuna necessità di scrivere codice JavaScript per ogni nuovo elemento di questo genere. Ci basta solo applicare il comportamento. Il gestore a livello di documento fa in modo che funzioni per ogni elemento nella pagina.
Possiamo pure combinare comportamenti multipli su un singolo elemento.
Il pattern âcomportamentaleâ può essere una alternativa a mini frammenti di JavaScript.
Riepilogo
Event delegation è davvero fico! Uno dei pattern più utili per gli eventi del DOM.
Spesso è usato per aggiungere dei gestori per molti elementi simili, ma non solo per quello.
Lâalgoritmo:
- Inserire un gestore singolo a un contenitore.
- Nel gestore â controlla lâelemento che ha originato lâevento con
event.target. - Se lâevento è avvenuto dentro un elemento che ci interessa, allora gestire lâevento.
Benefici:
- Semplifica lâinizializzazione e salva memoria: nessuna necessità di aggiungere molti gestori.
- Meno codice: aggiungendo o rimuovendo elemento non câè necessità di aggiungere e rimuovere gestori.
- Modifiche al DOM: possiamo aggiungere e rimuovere elementi in massa con
innerHTMLe simili.
Delegation ha i suoi limiti ovviamente:
- Per prima cosa, lâevento deve essere di tipo bubbling. Alcuni eventi non lo sono. Inoltre, i gestori di basso livello non dovrebbero usare
event.stopPropagation(). - Secondo, delegation può aggiungere carico alla CPU, perché il gestore a livello di container reagisce agli eventi di qualunque posizione del container, non importa se sono degni di nota o meno. Solitamente il carico è irrisorio, e quindi non lo prendiamo in minima considerazione.
Commenti
<code>, per molte righe â includile nel tag<pre>, per più di 10 righe â utilizza una sandbox (plnkr, jsbin, codepenâ¦)