Met de Invoker Commands API kunnen knoppen acties op doelelementen declaratief activeren met html-kenmerken.
Met deze aanpak zijn er minder handmatige event-listeners nodig en wordt de UI losgekoppeld van implementatiedetails.
Voor aangepaste, app-specifieke acties kun je je eigen opdrachtnamen definiëren. Aangepaste opdrachten moeten worden voorafgegaan door een dubbel streepje (--) om conflicten met toekomstige ingebouwde browseropdrachten te voorkomen.
Stapsgewijze implementatie
- Het doelelement definiëren: Identificeer het element dat op de actie reageert. Als het geen unieke
idheeft, voeg je er een toe. - Configureer de aanroepknop: Gebruik het kenmerk
commandforom naar deidvan het doel te verwijzen en het kenmerkcommandom de aangepaste opdrachtnaam op te geven (met het voorvoegsel--). - De opdrachtgebeurtenis verwerken: Voeg een
commandevent-listener rechtstreeks toe aan het doelelement. Het gebeurtenisobject bevat eencommand-property en eentarget-property (die verwijst naar het element dat wordt aangegeven doorcommandfor). - ARIA-statussen verwerken: Aangepaste opdrachten hebben geen inherente semantiek en je moet statussen zoals
aria-pressedofaria-expandedverwerken.
Voorbeeld: Aangepaste animatiebediening
<!-- 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>
Belangrijke beperkingen
- Voorvoegsel voor aangepaste opdrachten: VERPLICHT: Alle namen van aangepaste opdrachten moeten beginnen met
--(bijv.command="--my-action"). - Targeting: Het kenmerk
commandformoet overeenkomen met deidvan een element in dezelfde documentstructuur. - Geen bubbling: De gebeurtenis
commandbubbelt niet. Als er meerdere mogelijke doelen zijn, voeg je{ capture: true }toe aan de gebeurtenishandler en luister je naar een bovenliggend element. - Shadow roots: Als het doel zich in een shadow root bevindt, gebruik je
event.composedPath()[0]in plaats vanevent.target. - Toegankelijkheid: Aangepaste opdrachten hebben geen inherente semantiek en je moet statussen expliciet toepassen.
Overwegingen voor toegankelijkheid
- Als de knop aanvullende content toont, gebruik je het kenmerk
aria-expanded, dat standaard is ingesteld oparia-expanded="false"en oparia-expanded="true"als de bijbehorende content wordt getoond. - Als de knop zelf een interactie mogelijk maakt (voorbeelden zijn schakelknoppen die kunnen afspelen en pauzeren, volumeknoppen waarmee je het geluid kunt aan- en uitzetten), gebruik je het kenmerk
aria-pressed, ingesteld oparia-pressed="false"als de knop inactief is enaria-pressed="true"als de knop actief is. - Als de knop een element opent met de rol
menu,listbox,tree,gridofdialog, moet het bedieningselement een kenmerkaria-haspopuphebben met een waarde die is ingesteld op de rol van het element dat wordt geopend - Als de knop bij activering wordt uitgezet, gebruik je
aria-disabled="true"in plaats vandisabled, zodat de focus niet verloren gaat.
Reservestrategieën
Als de Invoker Commands API niet wordt ondersteund, wordt de gebeurtenis command niet geactiveerd. Voor volledige ondersteuning in alle moderne browsers raden we je aan de invokers-polyfill van https://github.com/keithamus/invokers-polyfill te gebruiken via npm install of CDN.
Deze polyfill ondersteunt aangepaste acties volledig (beginnend met --) en stuurt de gebeurtenis command op dezelfde manier als de native API.
Dynamisch importeren (prestatieoptimalisatie)
Voor de beste prestaties laad je de polyfill alleen als de browser de API niet standaard ondersteunt. Dit bespaart bandbreedte en verkort de uitvoeringstijd van scripts voor gebruikers in moderne browsers.
OPMERKING: Deze polyfill beheert geen ARIA-statussen (zoals aria-pressed of aria-expanded) voor aangepaste opdrachten. Je moet deze statussen handmatig synchroniseren in je event-listener om te zorgen dat je site toegankelijk is.
// 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);
}
});
Handmatige terugval (traditioneel patroon)
Als u liever geen polyfill gebruikt, kunt u een combinatie van gebeurtenisdelegatie gebruiken om gebeurtenissen te verzenden en een opdrachtregister om de acties te verwerken. Dit is een veelgebruikt architectuurpatroon in traditionele JavaScript-ontwikkeling dat zeer efficiënt en schaalbaar blijft.
// 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);
}
});