Cominciamo con un esempio.
Questo gestore è assegnato al <div>, ma viene eseguito anche se clicchiamo qualunque tag come ad esempio <em> oppure <code>:
<div onclick="alert('The handler!')">
<em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>
Non è un poâ strano? Perché il gestore su <div> viene eseguito se il click è su <em>?
Bubbling
Il principio alla base del bubbling è semplice.
Quando viene innescato un evento su un elemento, come prima cosa vengono eseguiti i gestori ad esso assegnati, poi ai nodi genitori, ed infine risale fino agli altri nodi antenati.
Poniamo il caso di avere 3 elementi annidati FORM > DIV > P con un gestore per ognuno di essi:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
Un click sul <p> interno innescherà onclick:
- Su questo
<p>. - Sul
<div>esterno. - Ed infine sul
<form>. - E così via fino a risalire fino allâoggetto
document.
Quindi se clicchiamo su <p>, vedremo 3 alerts: p â div â form.
Il processo viene soprannominato âbubblingâ, perché gli eventi si comportano come delle âbolleâ: dallâelemento interno vanno risalendo sempre più in alto, attraversando gli elementi genitori, come farebbe una bolla dâaria nellâacqua.
La parola chiave in questa frase è âquasiâ.
Lâevento focus, per esempio, non è tra questi. Ci sono anche altri esempi, che incontreremo. Ma ancora una volta è unâeccezione, piuttosto che una regola, in quanto la maggior parte degli eventi sono soggetti al bubbling.
event.target
Un gestore su un elemento genitore può sempre ottenere i dettagli relativi allâelemento che ha innescato lâevento.
Lâelemento annidato più in profondità che ha innescato lâevento viene chiamato elemento target, accessibile come event.target.
Notare le differenze rispetto a this (=event.currentTarget):
event.targetâ è lâelemento âtargetâ che ha innescato lâevento, ed esso non cambia durante il processo di bubbling.thisâ è lâelemento attuale, quello che sta eseguendo lâhandler su di esso.
Per esempio, se abbiamo un gestore singolo form.onclick, esso è in grado di âcatturareâ tutti i click dentro il form. Non importa dove sia avvenuto il click, risalirà fino al <form> ed eseguirà il gestore.
Nel gestore form.onclick:
this(=event.currentTarget) è lâelemento<form>, perché il gestore è in esecuzione su di esso.event.targetè lâelemento allâinterno del form che è stato cliccato.
Proviamolo:
form.onclick = function(event) {
event.target.style.backgroundColor = 'yellow';
// chrome needs some time to paint yellow
setTimeout(() => {
alert("target = " + event.target.tagName + ", this=" + this.tagName);
event.target.style.backgroundColor = ''
}, 0);
};form {
background-color: green;
position: relative;
width: 150px;
height: 150px;
text-align: center;
cursor: pointer;
}
div {
background-color: blue;
position: absolute;
top: 25px;
left: 25px;
width: 100px;
height: 100px;
}
p {
background-color: red;
position: absolute;
top: 25px;
left: 25px;
width: 50px;
height: 50px;
line-height: 50px;
margin: 0;
}
body {
line-height: 25px;
font-size: 16px;
}<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="example.css">
</head>
<body>
A click shows both <code>event.target</code> and <code>this</code> to compare:
<form id="form">FORM
<div>DIV
<p>P</p>
</div>
</form>
<script src="script.js"></script>
</body>
</html>Ã possibile che event.target equivalga a this â succede quando si clicca direttamente sullâelemento <form>.
Interrompere il bubbling
Un evento di bubbling va dallâelemento target verso lâalto, normalmente fino al tag <html>, poi fino allâoggetto document. Alcuni eventi arrivano ad incontrare lâoggetto window, chiamando tutti i gestori lungo il suo cammino.
Ogni gestore può decidere che quellâevento è stato completamente processato ed interrompere il bubbling.
Il metodo per fare ciò è event.stopPropagation().
Per esempio, qui body.onclick non funziona se clicchiamo su <button>:
<body onclick="alert(`il bubbling non arriva fino a qui`)">
<button onclick="event.stopPropagation()">Cliccami</button>
</body>
Se un elemento ha gestori multipli su un singolo elemento, allora se uno di questi interrompe il bubbling, gli altri continuano ad essere eseguiti.
In altri termini, event.stopPropagation() interrompe il bubbling da lì in poi, ma tutti gli altri gestori assegnati allâelemento corrente verranno eseguiti.
Per interrompere il bubbling ed evitare di eseguire i gestori sullâelemento attuale, câè il metodo event.stopImmediatePropagation(). Dopo di esso non verrà eseguito nessun altro gestore.
Il bubbling è conveniente. Non interromperlo senza una reale necessità : è ovvio ed architetturalmente ben congegnato.
Talvolta event.stopPropagation() nasconde piccole insidie che successivamente possono diventare problemi.
Ad esempio:
- Creiamo un menù annidato. Ogni sottomenù gestisce i click nei suoi elementi e chiama
stopPropagationin modo che gli altri menù non inneschino eventi. - Successivamente decidiamo di catturare i click nellâintera finestra, per tenere traccia dei comportamenti dellâutente (dove gli utenti cliccano). Alcuni sistemi analitici lo fanno. Solitamente il codice usa
document.addEventListener('click'â¦)per catturare tutti i click. - I nostri sistemi analitici non funzioneranno laddove i click vengano interrotti da
stopPropagation. E, purtroppo, avremo delle âzone morteâ.
In genere non ci sono reali necessità di interrompere il bubbling. Un problema che apparentemente lo richiede, può essere spesso risolto in altre maniere. Una di queste è quella di usare eventi personalizzati, di cui ci occuperemo più avanti. Possiamo anche scrivere i nostri dati dentro lâoggetto event in un gestore, e poi farli leggere da un altro, in questo modo possiamo passare ai gestori dei nodi genitori informazioni sui processi che avvengono più in basso.
Capturing
Câè unâaltra fase nellâelaborazione degli eventi, chiamata âcapturingâ. Viene usata raramente nel codice, ma talvolta può essere utile.
Lo standard DOM Events descrive 3 fasi nella propagazione dellâevento:
- Fase capturing â lâevento va sullâelemento.
- Fase target â lâevento ha raggiunto lâelemento target.
- Fase bubbling â lâevento risale su dallâelemento.
Ecco la figura estratta dalle specifiche, di un click su un <td> dentro una tabella:
Ossia: per un click su <td> lâevento prima attraversa la catena degli antenati e scende giù fino allâelemento (fase capturing), dopodiché raggiunge il target ed è lì che viene innescato (fase target), ed infine risale su (fase di bubbling) chiamando i gestori lungo il suo cammino.
Prima abbiamo accennato solo al bubbling, in quanto la fase capturing è usata raramente. Normalmente è assolutamente trasparente.
I gestori aggiunti usando le proprietà on<event> o tramite gli attributi HTML o usando solo due argomenti in addEventListener(event, handler) non sanno nulla della fase di capturing, ma verranno coinvolti solo nella seconda e terza fase.
Per catturare un evento in questa fase, abbiamo bisogno di impostare lâopzione capture a true nel gestore:
elem.addEventListener(..., {capture: true})
// oppure solamente "true" che è un alias per {capture: true}
elem.addEventListener(..., true)
Ci sono due possibili valori dellâopzione capture:
- Se
false(valore predefinito), il gestore è impostato nella fase di bubbling. - Se
true, il gestore è impostato nella fase di capturing.
Notare che mentre formalmente esistono 3 fasi, la seconda fase (âfase targetâ: lâevento ha raggiunto lâelemento) non viene gestita separatamente: i gestori, sia nella fase di capturing che nella fase di bubbling innescano in questa fase.
Vediamo le fasi di capturing e di bubbling in azione:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form>FORM
<div>DIV
<p>P</p>
</div>
</form>
<script>
for(let elem of document.querySelectorAll('*')) {
elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
}
</script>
Il codice imposta i gestori di click per ogni elemento nel documento per vedere quali sono in azione.
Se clicchiamo su <p>, la sequenza sarà :
HTMLâBODYâFORMâDIV(fase di capturing, il primo listener):P(fase target, viene innescata due volte, dato che abbiamo impostato due listener: capturing e bubbling)DIVâFORMâBODYâHTML(fase bubbling, il secondo listener).
La proprietà event.eventPhase restituisce il numero della fase in cui lâevento è stato catturato, ma viene usata raramente, dato che possiamo dedurla dal gestore.
removeEventListener ha bisogno della stessa faseSe abbiamo assegnato lâevento con addEventListener(..., true), allora siamo obbligati a fare menzione della stessa fase in removeEventListener(..., true) per rimuovere lâhandler con successo.
Nel caso in cui avessimo più gestori assegnati alla stessa fase, assegnati allo stesso evento, tramite addEventListener, verranno eseguiti secondo lâordine di creazione:
elem.addEventListener("click", e => alert(1)); // vi è garanzia che venga eseguito prima
elem.addEventListener("click", e => alert(2));
Riepilogo
Quando viene scatenato un evento, lâelemento più annidato viene etichettato come âelemento targetâ (event.target).
- Quindi lâevento si sposta in giù dalla root del documento fino allâ
event.target, chiamando gli eventi assegnati conaddEventListener(..., true)lungo il suo cammino (trueè una scorciatoia per{capture: true}). - Dopodiché vengono chiamati i gestori assegnati allâelemento stesso.
- Quindi gli eventi risalgono verso lâalto dallâ
event.targetalla root, chiamando i gestori assegnati tramiteon<event>, gli attributi HTML oppureaddEventListenerprivo però del terzo parametrofalse/{capture:false}.
Ogni handler accede alle proprietà dellâoggetto event:
event.targetâ Lâelemento più interno che ha generato lâevento.event.currentTarget(=this) â lâelemento che sta attualmente gestendo lâevento (quello che ha il gestore su di esso)event.eventPhaseâ la fase corrente (capturing=1, target=2, bubbling=3).
Qualunque gestore può interrompere la propagazione dellâevento chiamando event.stopPropagation(), ma non è raccomandabile, in quanto non possiamo essere del tutto sicuri che non ne abbiamo bisogno ai livelli superiori, magari per questioni del tutto differenti.
La fase di capturing viene usata molto raramente, solitamente gestiamo gli eventi nella fase di bubbling. Ed in questo câè una logica.
Nel mondo reale, quando avviene un incidente, prima intervengono le autorità locali. Loro conoscono meglio il posto dove è avvenuto. Quindi intervengono e autorità di alto livello in caso di necessità .
Per gli eventi è lo stesso. Il codice che assegna il gestore su un particolare elemento ha il massimo livello di dettagli dellâelemento e di cosa fa. Un gestore su un particolare <td> può essere adattato perfettamente per quel <td>, sa tutto su di esso, quindi dovrebbe avere la prima possibilità . Dopodiché i nodi immediatamente superiori conoscono il contesto, anche se un poâ meno, e così via fino ad arrivare al nodo di più alto livello che gestisce concetti generali e che quindi viene eseguito per ultimo.
Bubbling e Capturing gettano le basi per il concetto di âevent delegationâ un pattern di gestione degli eventi estremamente potente, che studieremo nel prossimo capitolo.
Commenti
<code>, per molte righe â includile nel tag<pre>, per più di 10 righe â utilizza una sandbox (plnkr, jsbin, codepenâ¦)