JavaScript: stopPropagation e preventDefault

Foto di Omar Gattis su Unsplash

Quando utilizziamo gli eventi in JavaScript possiamo utilizzare la fuzione addEventListener. La funzione accetta 3 parametri:

  • tipo di evento
  • la funzione da eseguire quando l’evento si manifesta
  • eventuali opzioni o useCapture

Se volessimo aggiungere un event listener ad un bottone della nostra pagina potremmo fare come segue

<button id="btn">Clicca qui</button>

<script>
    const btn = document.getElementById('btn');
    btn.addEventListener(
        'click',        // nome dell'evento
        function() {    // funzione eseguita quando faremo click sul bottone
            console.log('Click!')
        }
    )
</script>

Quando il codice è semplice come in questo caso non abbiamo particolari problemi. La situazione si complica quando ad esempio abbiamo una tabella dove:

  • il click su una riga apre il dettaglio della riga stessa;
  • il click su un bottone dentro la riga compie un’altra azione, nel nostro esempio elimina.

stopPropagation()

Scenario

Esempio di codice:

<div class="widget">
    <table>
        <tr>
            <td>Nome 1</td>
            <td><button>Elimina</button></td>
        </tr>
        <tr>
            <td>Nome 2</td>
            <td><button>Elimina</button></td>
        </tr>
    </table>
</div>

Otteniamo una tabella di questo tipo

Esempio tabella per stopPropagation

In questo esempio vogliamo impostare due event listener, uno per la riga intera e uno invece solo per il bottone Elimina. Se utilizzassimo del codice JavaScript come il seguente

// seleziono tutti i bottoni e tutte le righe
const btns = document.querySelectorAll('button');
const rows = document.querySelectorAll('tr')

// aggiungo l'event listener al click per i singoli bottoni
btns.forEach(function (btn) {
    btn.addEventListener(
        'click',
        function () { console.log('Eliminato'); }
    );
});

// aggiungo l'event listener al click per le singole righe
rows.forEach(function (btn) {
    btn.addEventListener(
        'click',
        function () { console.log('Dettagli'); }
    );
});

al click sulla riga avremmo correttamente il messaggio in console Dettagli ma al click sul bottone avremmo in console prima il messaggio Eliminato e appena dopo il messaggio Dettagli. In questo caso, nonostante la funzione riceva un evento come parametro, non utilizzandolo non lo abbiamo nemmeno scritto. Lo aggiungeremo tra poco.

Multipli eventi triggerati, perché avviene questo?

Quando un evento viene scatenato su un nodo del DOM, se nessuno glielo impedisce (vediamo tra poco come fare), questo evento risale tutta l’alberatura del nostro documento fino alla root. Ogni event listener che trova sulla strada e che è in ascolto dello stesso evento verrebbe triggerato.

Come possiamo ovviare?

Per evitare ciò e quindi essere gli unici che gestiscono l’evento possiamo usare il metodo stopPropagation() proprio di ogni evento.

Sappiamo che addEventListener passa un parametro di evento alla funzione, nel nostro esempio la chiameremo event. Quest’ultimo è un oggetto che rappreseta l’evento, con tutti i dettagli di dove e come è avvtenuto.

Soluzione

Modifichiamo quindi il nostro codice affiché, appena scatenato l’evento, venga richiamato il suo metodo stopPropagation().

const btns = document.querySelectorAll('td button');
const rows = document.querySelectorAll('tr')

btns.forEach(function (btn) {
    btn.addEventListener(
        'click',
        function (event) {  // <-- l'evento verrà passato dentro il parametro `event`
            event.stopPropagation();  // <-- fermo la propagazione
            console.log('Eliminato'); 
        }
    );
});

rows.forEach(function (btn) {
    btn.addEventListener(
        'click',
        function (event) {  // idem come sopra
            event.stopPropagation();  // idem come sopra
            console.log('Dettagli'); 
        }
    );
});

Se ora proviamo a cliccare sul bottone Elimina l’unico messaggio che vedremo in console sarà quello legato al event handler dei bottoni. La propagazione dell’evento è stata intercettata e fermata. Bene!

preventDefault()

Ci sono scenari dove vogliamo bloccare quello che è il comportamento di default della nostra pagina, ad esempio:

  • bloccare l’invio di una form se non ci sono le condizioni corrette;
  • evitare che il click su un link esegua la navigazione;
  • bloccare l’inserimento di caratteri non validi in un campo di input;
  • ecc

In queste situazioni vogliamo che, allo scatenarsi dell’evento, l’azione di default che verrebbe eseguita in condizioni normali non avvenga.

Scenario

Prendiamo per esempio il seguente codice:

<form>
    <div>
        <label for="check">Prima clicca qui</label>
        <input type="checkbox" id="check" />
    </div>

    <div>
        <label for="after">Poi potrai cliccare qui</label>
        <input type="checkbox" id="after" />
    </div>
</form>

Abbiamo due input di tipo checkbox. Vogliamo che il secondo sia cliccabile solo se il primo è checked, ovvero è stato attivato.

Esempio form per preventDefault

Soluzione

Possiamo scrivere del codice come il seguente che:

  • seleziona i due input
  • aggiunge un event listener al secondo identificato come after;
  • al click verifico se il primo input identificato come check è attivo;
    • se non lo è inibisco l’azione di default con il metodo preventDefault() dell’evento;
    • se lo è, non richiamo il metodo suddetto e così facendo non blocco il normale comportamento.
const inputCheck = document.getElementById('check');
const inputAfter = document.getElementById('after');

inputAfter.addEventListener(
    'click',
    function(event) {
        if (!inputCheck.checked) {
            event.preventDefault();
            console.log('Nope!')
        } else {
            console.log('Ok!')
        }                
    }
);

Conclusioni

Abbiamo visto come possiamo utilizzare i due metodi stopPropagation() e preventDefault() propri degli eventi per avere una più precisa e ottimale gestione dell’interattività della nostra pagina web. Questi metodi ci permettono di intercettare gli eventi e gestirli evitando concorrenze con altri event listener che possiamo avere nella nostra pagina.

Ci sono anche altri metodi e caratteristiche ovviamente che potete trovare qui:

Buon coding!