Introduzione
Quando una pagina contiene un solo campo di input, collegare un listener direttamente all’elemento è semplice. La situazione cambia quando dobbiamo gestire un form composto da molti campi, sezioni aggiungibili dinamicamente, messaggi di errore e controlli differenti in base al tipo di input.
In questi casi la delega degli eventi permette di registrare un numero ridotto di listener, applicandoli a un contenitore comune come il form. Grazie al meccanismo di bubbling, un evento generato da un campo risale verso i suoi elementi antenati. Il form può quindi intercettare l’evento e capire quale controllo lo ha originato tramite event.target.
In questo tutorial realizzeremo una validazione client-side per un form di registrazione. Gestiremo sia l’evento input, utile per controllare i dati mentre l’utente scrive, sia l’evento submit, necessario per una verifica finale. L’esempio mostrerà anche come validare elementi inseriti successivamente senza aggiungere nuovi listener.
Codice completo
<form id="registrationForm" novalidate>
<div class="field">
<label for="username">Nome utente</label>
<input
id="username"
name="username"
type="text"
data-rule="username"
aria-describedby="usernameError"
>
<small id="usernameError" class="error" aria-live="polite"></small>
</div>
<div class="field">
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
data-rule="email"
aria-describedby="emailError"
>
<small id="emailError" class="error" aria-live="polite"></small>
</div>
<div class="field">
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
data-rule="password"
aria-describedby="passwordError"
>
<small id="passwordError" class="error" aria-live="polite"></small>
</div>
<button type="submit">Crea account</button>
<p id="formMessage" role="status" aria-live="polite"></p>
</form>
<script>
const form = document.querySelector("#registrationForm");
const formMessage = document.querySelector("#formMessage");
const validators = {
username(value) {
if (value.trim().length < 3) {
return "Il nome utente deve contenere almeno 3 caratteri.";
}
return "";
},
email(value) {
const emailPattern = /^[^s@]+@[^s@]+.[^s@]+$/;
if (!emailPattern.test(value.trim())) {
return "Inserisci un indirizzo email valido.";
}
return "";
},
password(value) {
if (value.length < 8) {
return "La password deve contenere almeno 8 caratteri.";
}
return "";
}
};
function validateField(field) {
const ruleName = field.dataset.rule;
const validator = validators[ruleName];
// Se il campo non ha una regola, non viene validato.
if (!validator) {
return true;
}
const errorMessage = validator(field.value);
const errorElement = document.querySelector(
`#${field.getAttribute("aria-describedby")}`
);
field.setAttribute("aria-invalid", String(Boolean(errorMessage)));
if (errorElement) {
errorElement.textContent = errorMessage;
}
return errorMessage === "";
}
// Un solo listener gestisce tutti gli input presenti nel form.
form.addEventListener("input", (event) => {
const field = event.target;
if (!(field instanceof HTMLInputElement)) {
return;
}
if (!field.matches("[data-rule]")) {
return;
}
validateField(field);
formMessage.textContent = "";
});
// La delega funziona anche se nuovi input vengono aggiunti al form.
form.addEventListener("blur", (event) => {
const field = event.target;
if (!(field instanceof HTMLInputElement)) {
return;
}
if (field.matches("[data-rule]")) {
validateField(field);
}
}, true);
form.addEventListener("submit", (event) => {
event.preventDefault();
const fields = [...form.querySelectorAll("[data-rule]")];
const isFormValid = fields.every(validateField);
if (!isFormValid) {
formMessage.textContent =
"Controlla i campi evidenziati prima di continuare.";
return;
}
formMessage.textContent = "Registrazione completata correttamente.";
});
</script> Spiegazione
Perché usare la delega
Un approccio diretto potrebbe aggiungere un listener a ogni campo:
document.querySelector("#email").addEventListener("input", validateField);
document.querySelector("#password").addEventListener("input", validateField); Questa soluzione può funzionare, ma diventa fragile quando i campi vengono aggiunti o rimossi. Inoltre, il codice deve conoscere in anticipo tutti gli elementi da gestire.
Con la delega registriamo invece un listener sul form:
form.addEventListener("input", (event) => {
const field = event.target;
if (field.matches("[data-rule]")) {
validateField(field);
}
}); Il listener viene eseguito quando un qualsiasi elemento discendente genera un evento input. La condizione con matches() limita l’azione agli elementi che possiedono l’attributo data-rule.
Il ruolo di event.target
event.target rappresenta l’elemento originale che ha generato l’evento. Nel nostro esempio, se l’utente scrive nel campo email, event.target sarà proprio quell’input, anche se il listener è stato registrato sul form.
Il controllo instanceof HTMLInputElement aggiunge sicurezza: impedisce di trattare come campo un elemento diverso, ad esempio il pulsante di invio.
Perché blur richiede il terzo parametro true
L’evento blur normalmente non risale il DOM come molti altri eventi. Per intercettarlo dal form utilizziamo la fase di capturing, impostando il terzo argomento di addEventListener a true.
In questo modo il form può validare il campo quando l’utente lo abbandona. La verifica durante input offre un feedback immediato, mentre quella su blur evita di mostrare errori prima che l’utente abbia realmente interagito con il controllo.
Validazione durante submit
L’evento submit viene intercettato sul form, non sul pulsante. Questa scelta è importante perché il form può essere inviato anche premendo Invio dalla tastiera. event.preventDefault() blocca l’invio predefinito, consentendo di controllare tutti i campi prima di procedere.
Il metodo every() verifica che ogni campo sia valido. Anche se un campo non è corretto, il codice continua a validare gli altri, mostrando tutti gli errori in una sola operazione.
Best practice
- Registra i listener sul contenitore più appropriato: usa il form per gli eventi relativi ai suoi campi, evitando di delegare tutto a
document. - Filtra sempre il target: controlla il tipo dell’elemento e usa
matches()oclosest()per evitare effetti indesiderati. - Separa validazione e gestione degli eventi: la funzione
validateField()non dovrebbe sapere quale evento l’ha chiamata. - Usa attributi semantici:
aria-invalid,aria-describedbyearia-livemigliorano l’accessibilità dei messaggi. - Non affidarti solo al client: i controlli JavaScript migliorano l’esperienza utente, ma la validazione definitiva deve avvenire anche sul server.
- Evita listener duplicati: la delega è particolarmente utile quando componenti o campi vengono creati più volte durante la vita della pagina.
- Gestisci gli eventi una sola volta: non aggiungere nuovi listener ogni volta che aggiorni l’interfaccia.
Riepilogo
La combinazione di addEventListener e delega consente di costruire form più ordinati, estendibili ed efficienti. Invece di collegare un listener a ogni campo, possiamo ascoltare gli eventi sul form e identificare il controllo interessato attraverso event.target.
La strategia è particolarmente utile quando i campi sono numerosi o possono cambiare dinamicamente. Gli eventi input, blur e submit coprono rispettivamente il controllo durante la digitazione, la verifica al termine dell’interazione e la validazione finale.
