L'API Invoker Commands consente ai pulsanti di attivare azioni sugli elementi di destinazione in modo dichiarativo utilizzando gli attributi HTML.
Questo approccio riduce la necessità di listener di eventi manuali e disaccoppia l'interfaccia utente dai dettagli di implementazione.
Per le azioni personalizzate specifiche dell'applicazione, puoi definire i nomi dei comandi. I comandi personalizzati devono essere preceduti da un doppio trattino (--) per evitare conflitti con i futuri comandi integrati del browser.
Procedura di implementazione
- Definisci l'elemento target: identifica l'elemento che risponderà all'azione. Se non ha un
idunivoco, aggiungine uno. - Configura il pulsante di attivazione: utilizza l'attributo
commandforper puntare all'attributoiddel target e l'attributocommandper specificare il nome del comando personalizzato (con prefisso--). - Gestisci l'evento del comando: collega un listener di eventi
commanddirettamente all'elemento di destinazione. L'oggetto evento contiene una proprietàcommande una proprietàtarget(che fa riferimento all'elemento identificato dacommandfor). - Gestisci gli stati ARIA: i comandi personalizzati non hanno una semantica intrinseca e devi gestire stati come
aria-pressedoaria-expanded.
Esempio: controlli di animazione personalizzati
<!-- The target element that will respond to custom commands -->
<div id="action-target" class="target">
Action Target
</div>
<!-- Buttons declaratively linked to the target element -->
<!-- Each button sends a unique custom command starting with '--' -->
<button commandfor="action-target" command="--spin">
Spin
</button>
<button commandfor="action-target" command="--grow">
Grow
</button>
<button commandfor="action-target" command="--reset">
Reset All
</button>
<script>
// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
'--spin': (target, source) => {
const isSpun = target.classList.toggle('is-spun');
// Set ARIA states, as custom commands have no inherent semantics.
source?.setAttribute('aria-pressed', isSpun);
},
'--grow': (target, source) => {
const isGrown = target.classList.toggle('is-grown');
source?.setAttribute('aria-pressed', isGrown);
},
'--reset': (target) => {
target.classList.remove('is-spun', 'is-grown');
// Reset all associated buttons' ARIA states
document.querySelectorAll(`button[commandfor="${target.id}"]`).forEach(btn => {
btn.setAttribute('aria-pressed', 'false');
});
},
};
// 2. **Mandatory:** Listen for the 'command' event directly on the target element
// (This is necessary because the native 'command' event does not bubble)
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const source = event.source; // event.source refers to the triggering button
const action = commandRegistry[command];
if (action) {
action(target, source);
}
});
</script>
Limitazioni principali
- Prefisso dei comandi personalizzati: OBBLIGATORIO: tutti i nomi dei comandi personalizzati devono iniziare con
--(ad es.command="--my-action"). - Targeting: l'attributo
commandfordeve corrispondere all'attributoiddi un elemento nello stesso albero del documento. - Nessun bubbling: l'evento
commandnon genera bubbling. Se sono presenti più target possibili, aggiungi{ capture: true }al gestore di eventi e ascolta un predecessore. - Shadow root: se il target potrebbe trovarsi in uno shadow root, utilizza
event.composedPath()[0]anzichéevent.target. - Accessibilità: i comandi personalizzati non hanno una semantica intrinseca e devi applicare esplicitamente gli stati.
Considerazioni sull'accessibilità
- Se il pulsante mostra contenuti aggiuntivi, utilizza l'attributo
aria-expanded, impostato suaria-expanded="false"per impostazione predefinita e suaria-expanded="true"quando vengono visualizzati i contenuti associati. - Se il pulsante facilita un'interazione (ad esempio, pulsanti di attivazione/disattivazione che possono riprodurre e mettere in pausa, pulsanti del volume che possono disattivare e riattivare l'audio), utilizza l'attributo
aria-pressed, impostato suaria-pressed="false"quando non è attivo e suaria-pressed="true"quando è attivo. - Se il pulsante apre un elemento con un ruolo
menu,listbox,tree,gridodialog, il controllo deve avere un attributoaria-haspopupcon un valore impostato sul ruolo dell'elemento che apre - Se il pulsante viene disattivato all'attivazione, utilizza
aria-disabled="true"anzichédisabledper evitare la perdita dello stato attivo.
Strategie di riserva
Se l'API Invoker Commands non è supportata, l'evento command non verrà attivato. Per un supporto completo su tutti i browser moderni, ti consigliamo di utilizzare invokers-polyfill da https://github.com/keithamus/invokers-polyfill tramite npm install o CDN.
Questo polyfill supporta completamente le azioni personalizzate (a partire da --) e invia l'evento command esattamente come l'API nativa.
Importazione dinamica (ottimizzazione delle prestazioni)
Per un rendimento ottimale, devi caricare il polyfill solo se il browser non supporta l'API in modo nativo. In questo modo, si risparmia larghezza di banda e si riduce il tempo di esecuzione degli script per gli utenti che utilizzano browser moderni.
NOTA: questo polyfill non gestisce gli stati ARIA (come aria-pressed o aria-expanded) per i comandi personalizzati. Devi sincronizzare manualmente questi stati nel listener di eventi per assicurarti che il tuo sito sia accessibile.
// 1. Conditionally load the polyfill
const hasNativeSupport = 'commandForElement' in HTMLButtonElement.prototype;
if (!hasNativeSupport) {
// Wrap in an async IIFE to avoid top-level await issues in older browsers
(async () => {
try {
await import('https://esm.run/invokers-polyfill');
} catch (err) {
console.error('Error loading fallback:', err);
}
})();
}
// 2. Manually manage ARIA states in your listener
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const source = event.source; // The button that triggered the command
if (command === '--spin') {
const isSpun = target.classList.toggle('is-spun');
// Polyfill tip: Manually update ARIA to match the new state
source?.setAttribute('aria-pressed', isSpun);
}
});
Fallback manuale (pattern tradizionale)
Se preferisci non utilizzare un polyfill, puoi utilizzare una combinazione di delega degli eventi per inviare gli eventi e un registro dei comandi per gestire le azioni. Questo è un pattern architetturale comune nello sviluppo JavaScript tradizionale che rimane altamente efficiente e scalabile.
// 1. **Optional:** Define a registry of requested actions for cleaner logic
const commandRegistry = {
'--spin': (target) => target.classList.toggle('is-spun'),
'--grow': (target) => target.classList.toggle('is-grown'),
'--reset': (target) => target.classList.remove('is-spun', 'is-grown'),
};
// 2. If CommandEvent doesn't exist, we assume no native support and provide the fallback
if (!globalThis.CommandEvent) {
globalThis.CommandEvent = class CommandEvent extends Event {
constructor(type, { source, command, ...options } = {}) {
super(type, options);
this.source = source;
this.command = command;
}
}
}
// 3. The fallback: Dispatch events manually if native support is missing
document.addEventListener('click', (event) => {
const button = event.composedPath().find((el) => el.matches?.("button[commandfor]"));
if (!button) return;
const target = document.getElementById(button.getAttribute('commandfor'));
const command = button.getAttribute('command');
if (target && command) {
target.dispatchEvent(new CommandEvent('command', {
command,
source: button,
}));
}
});
// 4. **Mandatory:** Register the unified listener directly on the target element
document.getElementById('action-target').addEventListener('command', (event) => {
const command = event.command;
const target = event.target;
const action = commandRegistry[command];
if (action) {
action(target);
}
});