Approfondimento sugli eventi JavaScript

preventDefault e stopPropagation: quando utilizzare quali e cosa fanno esattamente ciascun metodo.

Event.stopPropagation() ed Event.preventDefault()

La gestione degli eventi JavaScript è spesso semplice. Ciò vale in particolar modo quando si ha a che fare con una struttura HTML semplice (relativamente piatta). Le cose, però, sono un po' più coinvolte quando gli eventi viaggiano (o si propagano) attraverso una gerarchia di elementi. In genere questo accade quando gli sviluppatori cercano stopPropagation() e/o preventDefault() per risolvere i problemi riscontrati. Se hai mai pensato: "Proverò solo preventDefault() e se non funziona proverò stopPropagation() e se non funziona, proverò entrambi", allora questo articolo fa al caso tuo. Ti spiegherò esattamente in cosa consiste ogni metodo e quando usarne uno e ti fornirò una serie di esempi pratici da esplorare. Il mio obiettivo è porre fine alla confusione una volta per tutte.

Tuttavia, prima di approfondire l'argomento, è importante brevemente accennare ai due tipi di gestione degli eventi possibili in JavaScript (in tutti i browser moderni, ovvero Internet Explorer prima della versione 9 non supportava l'acquisizione di eventi).

Stili di eventi (acquisizione e bolle)

Tutti i browser moderni supportano l'acquisizione di eventi, ma viene utilizzata molto raramente dagli sviluppatori. È interessante notare che si trattava dell'unica forma di evento originariamente supportata da Netscape. Il principale concorrente di Netscape, Microsoft Internet Explorer, non supportava l'acquisizione di eventi, ma supportava solo un altro stile di eventing chiamato bubbling degli eventi. Al momento della creazione del W3C, l'azienda ha trovato merito in entrambi gli stili di gestione degli eventi e ha dichiarato che i browser dovevano supportarli entrambi tramite un terzo parametro del metodo addEventListener. In origine, quel parametro era solo un valore booleano, ma tutti i browser moderni supportano un oggetto options come terzo parametro, che puoi utilizzare per specificare (tra gli altri elementi) se vuoi utilizzare o meno l'acquisizione di eventi:

someElement.addEventListener('click', myClickHandler, { capture: true | false });

Tieni presente che l'oggetto options è facoltativo, così come la relativa proprietà capture. Se uno dei due viene omesso, il valore predefinito di capture è false, che indica che verrà utilizzato il bubbling di eventi.

Acquisizione di eventi

Che cosa significa se il gestore di eventi "sta in ascolto in fase di acquisizione"? Per capirlo, dobbiamo sapere come hanno origine gli eventi e come viaggiano. Quanto segue è valido per tutti gli eventi, anche se tu, in qualità di sviluppatore, non li utilizzi, non ti interessa o non ne pensi.

Tutti gli eventi iniziano nella finestra e passano prima attraverso la fase di acquisizione. Ciò significa che, quando viene inviato un evento, inizia la finestra e si sposta "verso il basso" verso il suo elemento targetper primo. Questo accade anche se stai ascoltando solo nella fase bubbling. Considera il seguente esempio di markup e JavaScript:

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('#C was clicked');
  },
  true,
);

Quando un utente fa clic sull'elemento #C, viene inviato un evento che ha origine nel window. Questo evento verrà quindi propagato tra i suoi discendenti come segue:

window => document => <html> => <body> => e così via, fino a raggiungere il target.

Non è importante se nulla sta ascoltando un evento di clic nell'elemento window, document o <html>, nell'elemento <body> (o in qualsiasi altro elemento sul percorso verso il target). Un evento ha comunque origine al punto window e inizia il suo percorso come appena descritto.

Nel nostro esempio, l'evento di clic propagherà (questa è una parola importante in quanto si collega direttamente al funzionamento del metodo stopPropagation() e verrà spiegato più avanti in questo documento) da window al relativo elemento di destinazione (in questo caso, #C) attraverso ogni elemento tra window e #C.

Ciò significa che l'evento di clic inizierà alle ore window e il browser porrà le seguenti domande:

"C'è qualcosa in ascolto di un evento di clic su window in fase di acquisizione?" In tal caso, verranno attivati i gestori di eventi appropriati. Nel nostro esempio, niente è, quindi nessun gestore si attiverà.

Successivamente, l'evento verrà propagato a document e il browser chiederà: "C'è qualcosa in ascolto di un evento clic su document in fase di acquisizione?" In tal caso, verranno attivati i gestori di eventi appropriati.

Successivamente, l'evento verrà propagato all'elemento <html> e il browser chiederà: "C'è qualcosa in ascolto di un clic sull'elemento <html> in fase di acquisizione?" In tal caso, verranno attivati i gestori di eventi appropriati.

Successivamente, l'evento verrà propagato all'elemento <body> e il browser chiederà: "C'è qualcosa in ascolto di un evento clic sull'elemento <body> in fase di acquisizione?" In tal caso, verranno attivati i gestori di eventi appropriati.

Successivamente, l'evento verrà propagato all'elemento #A. Di nuovo, il browser chiederà se è in ascolto di un evento di clic su #A in fase di acquisizione. In tal caso, verranno attivati i gestori di eventi appropriati.

Successivamente, l'evento verrà propagato all'elemento #B (e verrà posta la stessa domanda).

Infine, l'evento raggiungerà il target e il browser chiederà: "C'è qualcosa in ascolto di un evento clic sull'elemento #C in fase di acquisizione?" Questa volta la risposta è "sì!" Questo breve periodo di tempo in cui l'evento si trova in corrispondenza del target, è noto come "fase target". A questo punto, si attiverà il gestore di eventi, il browser console.log "#C was Click" e così abbiamo finito? Sbagliato! Non abbiamo niente. Il processo continua, ma ora passa alla fase bubbling.

Bollicine di eventi

Il browser chiederà:

"C'è qualcosa in ascolto di un evento di clic su #C nella fase bubbling?" Fai attenzione qui. È assolutamente possibile ascoltare i clic (o qualsiasi tipo di evento) in entrambe le fasi di acquisizione e bubbling. Se avessi collegato gestori di eventi in entrambe le fasi (ad es. chiamando .addEventListener() due volte, una volta con capture = true e una volta con capture = false), allora sì, entrambi i gestori di eventi si attiverebbero per lo stesso elemento. È importante anche notare che gli attacchi vengono attivati in fasi diverse (una durante la fase di acquisizione e l'altra nella fase di bubbling).

Successivamente, l'evento propagerà (più comunemente indicato come "bolla" perché sembra che l'evento si sposti "in cima" all'albero DOM) verso l'elemento principale #B e il browser chiederà: "C'è qualcosa in ascolto di eventi di clic su #B nella fase bubbling?" Nel nostro esempio, niente è, quindi nessun gestore si attiva.

Dopodiché, l'evento apparirà nel fumetto #A e il browser chiederà: "C'è qualcosa in ascolto di eventi di clic su #A nella fase bubbling?"

Dopodiché, il fumetto dell'evento mostrerà <body>: "C'è qualcosa in ascolto di eventi di clic sull'elemento <body> nella fase bubbling?"

Poi, l'elemento <html>: "C'è qualcosa in ascolto di eventi di clic sull'elemento <html> nella fase bubbling?

Poi, la domanda document: "C'è qualcosa in ascolto di eventi di clic su document in fase bubbling?"

Infine, l'istruzione window: "C'è qualcosa in ascolto di eventi di clic nella finestra nella fase bubbling?"

Finalmente. È stato un viaggio lungo e probabilmente il nostro evento ormai è molto stanco, ma che tu ci creda o no, questo è il viaggio che ogni evento attraversa. Nella maggior parte dei casi, questo non viene mai notato perché in genere gli sviluppatori sono interessati solo a una fase degli eventi o all'altra (che di solito è la fase bubbling).

È consigliabile dedicare un po' di tempo all'acquisizione e al bubbling degli eventi, nonché alla registrazione di alcune note nella console durante l'attivazione dei gestori. È molto interessante vedere il percorso di un evento. Ecco un esempio che ascolta ogni elemento in entrambe le fasi.

<html>
  <body>
    <div id="A">
      <div id="B">
        <div id="C"></div>
      </div>
    </div>
  </body>
</html>
document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in capturing phase');
  },
  true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in capturing phase');
  },
  true,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in capturing phase');
  },
  true,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in capturing phase');
  },
  true,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in capturing phase');
  },
  true,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in capturing phase');
  },
  true,
);

document.addEventListener(
  'click',
  function (e) {
    console.log('click on document in bubbling phase');
  },
  false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('click on <html> in bubbling phase');
  },
  false,
);
document.body.addEventListener(
  'click',
  function (e) {
    console.log('click on <body> in bubbling phase');
  },
  false,
);
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('click on #A in bubbling phase');
  },
  false,
);
document.getElementById('B').addEventListener(
  'click',
  function (e) {
    console.log('click on #B in bubbling phase');
  },
  false,
);
document.getElementById('C').addEventListener(
  'click',
  function (e) {
    console.log('click on #C in bubbling phase');
  },
  false,
);

L'output della console dipende dall'elemento su cui fai clic. Se dovessi fare clic sull'elemento "più profondo" nell'albero DOM (l'elemento #C), vedrai ogni singolo gestore di eventi attivato. Con un po' di stile CSS per rendere più evidente quale sia l'elemento, ecco l'elemento #C di output della console (con anche uno screenshot):

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"

Puoi giocare in modo interattivo con la demo dal vivo riportata di seguito. Fai clic sull'elemento #C e osserva l'output della console.

event.stopPropagation()

Dopo aver compreso da dove hanno origine gli eventi e come si propagano (ossia si propagano) nel DOM sia nella fase di acquisizione che in quella bollire, ora possiamo rivolgere la nostra attenzione su event.stopPropagation().

Il metodo stopPropagation() può essere chiamato per la maggior parte degli eventi DOM nativi. Dico "maggior parte" perché ce ne sono alcuni in cui la chiamata a questo metodo non funziona (perché l'evento non si propaga per iniziare). Eventi come focus, blur, load, scroll e alcuni altri rientrano in questa categoria. Puoi chiamare stopPropagation(), ma non accadrà nulla di interessante, poiché questi eventi non si propagano.

Ma che cosa fa stopPropagation?

In pratica, è proprio quello che dice. Quando lo chiami, l'evento cesserà, da quel momento, di propagarsi a tutti gli elementi a cui altrimenti si recherebbe. Questo vale per entrambe le direzioni (acquisizione e bolle). Quindi, se chiami stopPropagation() in qualsiasi punto della fase di acquisizione, l'evento non arriverà mai alla fase target o bubbling. Se la chiami in fase di bubbling, avrà già superato la fase di acquisizione, ma smetterà di "bubbling" dal punto in cui l'hai chiamata.

Tornando allo stesso markup di esempio, cosa pensi che accadrebbe se chiamassimo stopPropagation() nella fase di acquisizione dell'elemento #B?

Riuscirà a ottenere il seguente output:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"

Puoi giocare in modo interattivo con la demo dal vivo riportata di seguito. Fai clic sull'elemento #C nella demo dal vivo e osserva l'output della console.

Che ne dici di interrompere la propagazione in #A nella fase bubbling? Ciò comporterebbe il seguente output:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"

Puoi giocare in modo interattivo con la demo dal vivo riportata di seguito. Fai clic sull'elemento #C nella demo dal vivo e osserva l'output della console.

Ancora una, solo per divertimento. Che cosa succede se chiamiamo stopPropagation() nella fase target per #C? Ricorda che "fase target" è il nome assegnato al periodo di tempo in cui l'evento si trova in corrispondenza del suo target. Riuscirà a ottenere il seguente output:

"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"

Tieni presente che il gestore di eventi per #C in cui registriamo "clic su #C nella fase di acquisizione" continua viene eseguito, al contrario quello in cui registriamo il "clic su #C nella fase bubbling". Questo dovrebbe avere un senso perfetto. Abbiamo chiamato stopPropagation() dal primo, quindi è questo il punto in cui verrà interrotta la propagazione dell'evento.

Puoi giocare in modo interattivo con la demo dal vivo riportata di seguito. Fai clic sull'elemento #C nella demo dal vivo e osserva l'output della console.

In una di queste demo dal vivo, ti invito a provarlo. Prova a fare clic solo sull'elemento #A o solo sull'elemento body. Prova a prevedere cosa accadrà e poi osserva se hai ragione. A questo punto, dovresti essere in grado di fare previsioni abbastanza precise.

event.stopImmediatePropagation()

Che cos'è questo metodo strano e non usato spesso? È simile a stopPropagation, ma anziché impedire a un evento di spostarsi verso discendenti (acquisizione) o predecessori (bubbling), questo metodo si applica solo quando è presente più di un gestore di eventi collegato a un singolo elemento. Poiché addEventListener() supporta uno stile multicast di eventi, è del tutto possibile collegare un gestore di eventi a un singolo elemento più volte. In questi casi, nella maggior parte dei browser, i gestori di eventi vengono eseguiti nell'ordine in cui sono stati collegati. La chiamata a stopImmediatePropagation() impedisce l'attivazione di tutti i gestori successivi. Considera l'esempio seguente:

<html>
  <body>
    <div id="A">I am the #A element</div>
  </body>
</html>
document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run first!');
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I shall run second!');
    e.stopImmediatePropagation();
  },
  false,
);

document.getElementById('A').addEventListener(
  'click',
  function (e) {
    console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
  },
  false,
);

L'esempio riportato sopra restituirà il seguente output della console:

"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"

Tieni presente che il terzo gestore di eventi non viene mai eseguito perché il secondo gestore di eventi chiama e.stopImmediatePropagation(). Se invece avessimo chiamato e.stopPropagation(), il terzo gestore verrebbe comunque eseguito.

event.preventDefault()

Se stopPropagation() impedisce a un evento di spostarsi "verso il basso" (acquisizione) o "verso l'alto" (bollente), che cosa fa preventDefault()? Sembra che faccia qualcosa di simile. È vero?

In effetti, no. Sebbene vengano spesso confusi, in realtà non hanno molto a che fare tra loro. Quando vedi preventDefault(), aggiungi la parola "azione" in testa. Prova a "impedire l'azione predefinita".

E qual è l'azione predefinita che potresti chiedere? Sfortunatamente, la risposta non è così chiara, perché dipende in gran parte dalla combinazione di elemento + evento. E per rendere le cose ancora più confuse, a volte non esiste alcuna azione predefinita.

Iniziamo con un esempio molto semplice da capire. Che cosa ti aspetti che succede quando fai clic su un link in una pagina web? Ovviamente, ti aspetti che il browser acceda all'URL specificato dal link in questione. In questo caso, l'elemento è un anchor tag e l'evento è un evento di clic. Questa combinazione (<a> + click) ha un'"azione predefinita" per accedere all' href del link. E se volessi impedire al browser di eseguire l'azione predefinita? In altre parole, supponiamo di voler impedire al browser di accedere all'URL specificato dall'attributo href dell'elemento <a>. Questo è ciò che preventDefault() farà per te. Considera questo esempio:

<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
  'click',
  function (e) {
    e.preventDefault();
    console.log('Maybe we should just play some of their music right here instead?');
  },
  false,
);

Puoi giocare in modo interattivo con la demo dal vivo riportata di seguito. Fai clic sul link The Avett Brothers e controlla l'output della console (e il fatto che il tuo sito non sia reindirizzato al sito web di Avett Brothers).

Normalmente, se fai clic sul link The Avett Brothers, si accede a www.theavettbrothers.com. In questo caso, tuttavia, abbiamo collegato un gestore di eventi di clic all'elemento <a> e specificato che l'azione predefinita non deve essere consentita. Pertanto, quando un utente fa clic su questo link, non viene indirizzato da nessuna parte e la console registra semplicemente il messaggio "Magari dovremmo riprodurre un po' della sua musica qui?"

Quali altre combinazioni di elemento/evento ti consentono di impedire l'azione predefinita? Non posso elencarli tutti e, a volte, è sufficiente fare delle prove per vederli. Eccone alcuni, in breve:

  • L'elemento <form> + evento "submit": preventDefault() per questa combinazione impedirà l'invio di un modulo. Questo è utile se vuoi eseguire la convalida e se qualcosa non funziona, puoi chiamare preventDefault in modo condizionale per interrompere l'invio del modulo.

  • Elemento <a> + evento "click": preventDefault() per questa combinazione impedisce al browser di accedere all'URL specificato nell'attributo href dell'elemento <a>.

  • document + evento "mousewheel": preventDefault() per questa combinazione impedisce lo scorrimento della pagina con la rotellina del mouse (anche se lo scorrimento con la tastiera funziona comunque).
    ↜ È necessario chiamare addEventListener() con { passive: false }.

  • document + evento "keydown": preventDefault() per questa combinazione è letale. Rendi la pagina in gran parte inutile, impedendo lo scorrimento da tastiera, il passaggio con il tasto Tab e l'evidenziazione della tastiera.

  • document + evento "mousedown": preventDefault() per questa combinazione impedirà l'evidenziazione del testo con il mouse e qualsiasi altra azione "predefinita" che viene richiamata con il mouse down.

  • Elemento <input> + evento "keypress": preventDefault() per questa combinazione impedirà ai caratteri digitati dall'utente di raggiungere l'elemento di input (ma non farlo perché esiste raramente, se non è, un motivo valido).

  • document + evento "contextmenu": preventDefault() per questa combinazione impedisce la visualizzazione del menu contestuale del browser nativo quando un utente fa clic con il tasto destro del mouse o preme a lungo (o in qualsiasi altro modo in cui potrebbe essere visualizzato un menu contestuale).

Questo non è un elenco esaustivo in alcun modo, ma si spera che ti dia una buona idea di come utilizzare preventDefault().

Una barzelletta divertente?

Cosa succede se stopPropagation() e preventDefault() nella fase di acquisizione, a partire dal documento? Oggi è il momento di festeggiare. Il seguente snippet di codice renderà completamente inutili qualsiasi pagina web:

function preventEverything(e) {
  e.preventDefault();
  e.stopPropagation();
  e.stopImmediatePropagation();
}

document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });

Non so proprio perché ti piaccia farlo (tranne magari per fare una barzelletta su qualcuno), ma è utile pensare a cosa sta succedendo qui e capire perché crea la situazione in cui si verifica.

Tutti gli eventi hanno origine da window, quindi in questo snippet smettiamo di funzionare, tutti gli eventi click, keydown, mousedown, contextmenu e mousewheel non potranno raggiungere elementi che potrebbero ascoltarli. Chiamiamo anche stopImmediatePropagation in modo che tutti i gestori collegati al documento dopo questo, vengano neutralizzati.

Tieni presente che stopPropagation() e stopImmediatePropagation() non sono (almeno non principalmente) ciò che rende inutile la pagina. Semplicemente, impediscono agli eventi di arrivare a destinazione altrimenti.

Tuttavia, chiamiamo anche preventDefault() e, come ricorderai, impedisci l'azione predefinita. Pertanto, tutte le azioni predefinite (come lo scorrimento della rotellina del mouse, lo scorrimento da tastiera, l'evidenziazione o il tasto Tab, il clic su un link, la visualizzazione del menu contestuale e così via) vengono tutte bloccate, lasciando la pagina in uno stato abbastanza inutile.

Demo dal vivo

Per esplorare nuovamente tutti gli esempi di questo articolo in un unico posto, dai un'occhiata alla demo incorporata riportata di seguito.

Ringraziamenti

Immagine hero di Tom Wilson su Unsplash.