In JavaScript il sistema di eventi non è limitato a clic, input o caricamenti di pagina. Possiamo creare eventi personalizzati (custom events) per far comunicare in modo pulito parti diverse dell'applicazione, senza accoppiarle direttamente.
Perché usare eventi custom
- Per disaccoppiare componenti: chi emette l'evento non conosce chi lo ascolta.
- Per centralizzare la logica che deve reagire a certi avvenimenti.
- Per sostituire catene di callback o passaggi complessi di funzioni.
L'idea è semplice: definisci un evento con un nome (es. user:logged-in),
lo scateni quando serve, e altrove nel codice ti registri per ascoltarlo.
Creare un evento custom con CustomEvent
L'oggetto base è il costruttore CustomEvent. Prende due argomenti:
- nome dell'evento (stringa);
- opzioni, un oggetto con varie proprietà, tra cui
detail.
// Creazione di un evento custom
const loginEvent = new CustomEvent("user:login", {
detail: {
username: "alice",
time: new Date()
},
bubbles: true, // partecipa al bubbling nel DOM
cancelable: true // l'evento può essere annullato
});
La proprietà detail può contenere qualunque dato ti serva per descrivere
meglio cosa è successo: oggetti, stringhe, numeri, ecc.
Dispatch: scatenare l'evento
Una volta creato l'evento, va dispatched su un nodo del DOM usando
dispatchEvent. Qualsiasi EventTarget può emettere eventi:
elementi del DOM, window, document, ecc.
// Supponiamo di avere un elemento radice
const root = document.querySelector("#app-root");
root.dispatchEvent(loginEvent);
Tutti i listener che ascoltano l'evento user:login su root
(o su un suo antenato, se l'evento fa bubbling) verranno eseguiti.
Ascoltare un evento custom
Per ascoltare un evento custom si usa addEventListener, esattamente
come per gli eventi nativi:
const root = document.querySelector("#app-root");
root.addEventListener("user:login", function (event) {
console.log("Utente loggato:", event.detail.username);
console.log("Ora del login:", event.detail.time);
});
L'oggetto event è un normale evento DOM, con in più la proprietà
detail dove trovi i dati passati alla creazione dell'evento.
Esempio completo: flusso di login
Immagina di avere un modulo di login. Quando l'utente effettua correttamente il login, invece di chiamare direttamente decine di funzioni sparse, scateni un evento.
<button id="login-btn">Login</button>
<p id="status"></p>
// Riferimenti agli elementi
const loginButton = document.querySelector("#login-btn");
const statusParagraph = document.querySelector("#status");
// Listener che reagisce all'evento custom
document.addEventListener("user:login", function (event) {
const user = event.detail.username;
statusParagraph.textContent = `Benvenuto, ${user}!`;
});
// Simulazione di una funzione di login
function fakeLogin(username) {
// ... qui ci sarebbe la chiamata al server ecc.
const loginEvent = new CustomEvent("user:login", {
detail: { username },
bubbles: true
});
document.dispatchEvent(loginEvent);
}
// Click sul bottone di login
loginButton.addEventListener("click", function () {
fakeLogin("alice");
});
In questo modo il codice che gestisce l'interfaccia utente non ha bisogno di conoscere i dettagli di come avviene il login. Si limita ad ascoltare l'evento.
bubbles e propagazione nel DOM
Come per gli eventi nativi, gli eventi custom possono:
- rimanere confinati all'elemento su cui sono dispatchati;
- oppure propagarsi verso l'alto tramite bubbling.
La proprietà bubbles controlla questo comportamento.
const child = document.querySelector("#child");
const parent = document.querySelector("#parent");
parent.addEventListener("my:event", function () {
console.log("Evento catturato dal parent");
});
child.addEventListener("my:event", function () {
console.log("Evento catturato dal child");
});
// Evento che fa bubbling
const bubblingEvent = new CustomEvent("my:event", { bubbles: true });
// Dispatch sul child
child.dispatchEvent(bubblingEvent);
// Output in console:
// "Evento catturato dal child"
// "Evento catturato dal parent" (per via del bubbling)
Se imposti bubbles: false, l'evento verrà gestito solo sull'elemento
di dispatch e non risalirà la gerarchia del DOM.
Eventi cancellabili: cancelable e preventDefault
Un evento custom può essere cancellabile. Ciò significa che un listener
può impedirne l'azione predefinita chiamando event.preventDefault(),
a patto che alla creazione dell'evento tu abbia impostato cancelable: true.
// Creazione dell'evento
const deleteEvent = new CustomEvent("item:delete", {
detail: { id: 123 },
cancelable: true
});
// Un listener che potrebbe bloccare l'operazione
document.addEventListener("item:delete", function (event) {
const conferma = window.confirm("Sei sicuro di voler eliminare l'elemento?");
if (!conferma) {
event.preventDefault();
}
});
// Dispatch dell'evento e controllo del risultato
const eseguiCancellazione = document.dispatchEvent(deleteEvent);
if (eseguiCancellazione) {
console.log("Procedo con la cancellazione nel database...");
} else {
console.log("Cancellazione annullata dall'utente.");
}
Il valore di ritorno di dispatchEvent è:
truese nessun listener ha chiamatopreventDefault();falsealtrimenti.
Convenzioni sui nomi degli eventi
Non esiste un obbligo rigido sui nomi, ma alcune convenzioni aiutano a mantenere ordine nel codice:
- Usa prefissi per indicare il contesto, ad esempio
user:login,cart:item-added. - Preferisci nomi descrittivi:
modal:openedè meglio dimodal:ok. - Evita di riutilizzare nomi di eventi nativi (come
click,input).
Creare un semplice event bus con document
Un pattern molto comune è usare document o window come
una sorta di event bus globale. Tutti dispatchano e ascoltano eventi
sullo stesso oggetto, e questo riduce il numero di riferimenti diretti tra moduli.
// emitter.js
export function emit(name, detail) {
const event = new CustomEvent(name, { detail });
document.dispatchEvent(event);
}
// listener.js
export function on(name, handler) {
document.addEventListener(name, handler);
}
// altrove nel codice
import { emit } from "./emitter.js";
import { on } from "./listener.js";
on("cart:item-added", function (event) {
console.log("Nuovo elemento nel carrello:", event.detail);
});
emit("cart:item-added", { id: 1, name: "Libro JS" });
Questo approccio è semplice ma potente per progetti piccoli e medi. Per progetti più grandi, è consigliabile passare a soluzioni come Redux, EventEmitter custom, o altri sistemi di gestione dello stato e degli eventi.
Eventi custom in componenti web
Se utilizzi Web Components (custom elements), gli eventi custom sono il meccanismo principale per comunicare verso l'esterno. Il componente emette un evento quando cambia qualcosa, e il codice esterno decide cosa fare.
class CounterButton extends HTMLElement {
constructor() {
super();
this.count = 0;
const button = document.createElement("button");
button.textContent = "Cliccami";
button.addEventListener("click", () => {
this.count++;
this.dispatchEvent(new CustomEvent("counter:change", {
detail: { count: this.count },
bubbles: true
}));
});
this.appendChild(button);
}
}
customElements.define("counter-button", CounterButton);
<counter-button id="counter"></counter-button>
<p id="count-output">Count: 0</p>
const counter = document.querySelector("#counter");
const output = document.querySelector("#count-output");
counter.addEventListener("counter:change", function (event) {
output.textContent = "Count: " + event.detail.count;
});
Qui il componente resta indipendente da chi lo usa: si limita a emettere eventi quando il suo stato cambia.
Buone pratiche
- Non abusare degli eventi custom: usali quando servono davvero a disaccoppiare moduli.
- Documenta sempre i nomi degli eventi e la struttura di
detail. - Mantieni coerente lo stile di denominazione (prefissi, separatori, tempi verbali).
- Evita di passare oggetti enormi in
detail: meglio dati mirati.
Conclusione
Gli eventi custom sono uno strumento fondamentale per costruire applicazioni JavaScript
modulari e manutenibili. Usando CustomEvent, dispatchEvent e
addEventListener puoi modellare i cambiamenti di stato della tua app come
eventi significativi, facilmente osservabili da qualunque parte del codice.
Sperimenta introducendo un paio di eventi custom nel tuo progetto corrente e osserva come cambia l'organizzazione del codice: spesso è il primo passo verso un design più chiaro e scalabile.