Invoker Commands API memungkinkan tombol memicu tindakan pada elemen target secara deklaratif menggunakan atribut HTML.
Pendekatan ini mengurangi kebutuhan akan pemroses peristiwa manual dan memisahkan UI dari detail implementasi.
Untuk tindakan kustom khusus aplikasi, Anda dapat menentukan nama perintah Anda sendiri. Perintah kustom harus diawali dengan tanda hubung ganda (--) untuk menghindari konflik dengan perintah browser bawaan di masa mendatang.
Langkah-langkah implementasi
- Tentukan elemen target: Identifikasi elemen yang akan merespons tindakan. Jika tidak memiliki
idyang unik, tambahkan. - Konfigurasi tombol pemanggil: Gunakan atribut
commandforuntuk mengarah keidtarget, dan atributcommanduntuk menentukan nama perintah kustom (diawali dengan--). - Menangani peristiwa perintah: Lampirkan pemroses peristiwa
commandlangsung pada elemen target. Objek peristiwa berisi properticommanddan propertitarget(mengacu pada elemen yang diidentifikasi olehcommandfor). - Menangani status aria: Perintah kustom tidak memiliki semantik bawaan, dan Anda harus menangani status seperti
aria-pressedatauaria-expanded.
Contoh: Kontrol Animasi Kustom
<!-- 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>
Batasan kunci
- Awalan perintah kustom: WAJIB: Semua nama perintah kustom harus diawali dengan
--(misalnya,command="--my-action"). - Penargetan: Atribut
commandforharus cocok denganidelemen di hierarki dokumen yang sama. - Tidak ada bubbling: Peristiwa
commandtidak melakukan bubbling. Jika ada beberapa kemungkinan target, tambahkan{ capture: true }ke pengendali peristiwa dan proses di ancestor. - Shadow root: Jika target mungkin berada di shadow root, gunakan
event.composedPath()[0], bukanevent.target. - Aksesibilitas: Perintah kustom tidak memiliki semantik bawaan, dan Anda harus menerapkan status apa pun secara eksplisit.
Pertimbangan aksesibilitas
- Jika tombol menampilkan konten tambahan, gunakan atribut
aria-expanded, yang ditetapkan kearia-expanded="false"secara default danaria-expanded="true"saat konten pengiring ditampilkan - Jika tombol itu sendiri memfasilitasi interaksi (contohnya termasuk tombol beralih yang dapat memutar dan menjeda, tombol volume yang dapat membisukan dan membunyikan audio), gunakan atribut
aria-pressed, yang ditetapkan kearia-pressed="false"saat tidak aktif, danaria-pressed="true"saat aktif - Jika tombol membuka elemen dengan peran
menu,listbox,tree,grid, ataudialog, kontrol harus memiliki atributaria-haspopup, dengan nilai yang ditetapkan ke peran elemen yang dibuka - Jika tombol menjadi nonaktif saat diaktifkan, gunakan
aria-disabled="true", bukandisabled, agar fokus tidak hilang
Strategi penggantian
Jika Invoker Commands API tidak didukung, peristiwa command tidak akan diaktifkan. Untuk dukungan penuh di semua browser modern, sebaiknya gunakan invokers-polyfill dari https://github.com/keithamus/invokers-polyfill melalui npm install atau CDN.
Polyfill ini sepenuhnya mendukung tindakan kustom (dimulai dengan --) dan mengirimkan peristiwa command persis seperti API native.
Dynamic Import (Pengoptimalan Performa)
Untuk performa terbaik, Anda hanya boleh memuat polyfill jika browser tidak mendukung API secara native. Tindakan ini menghemat bandwidth dan mengurangi waktu eksekusi skrip bagi pengguna di browser modern.
CATATAN: Polyfill ini tidak mengelola status ARIA (seperti aria-pressed atau aria-expanded) untuk perintah kustom. Anda harus menyinkronkan status ini secara manual di pemroses peristiwa untuk memastikan situs Anda dapat diakses.
// 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);
}
});
Penggantian manual (Pola tradisional)
Jika tidak ingin menggunakan polyfill, Anda dapat menggunakan kombinasi pendelegasian peristiwa untuk mengirim peristiwa dan registry perintah untuk menangani tindakan. Ini adalah pola arsitektur umum dalam pengembangan JavaScript tradisional yang tetap sangat efisien dan skalabel.
// 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);
}
});