Invoker Commands API позволяет кнопкам запускать действия с целевыми элементами декларативно с помощью HTML-атрибутов.
Такой подход снижает потребность в ручных прослушивателях событий и отделяет пользовательский интерфейс от деталей реализации.
Для специальных действий, связанных с конкретным приложением, можно задать собственные названия команд. Специальные команды должны начинаться с двойного дефиса (--), чтобы избежать конфликтов со встроенными командами браузера, которые могут появиться в будущем.
Реализация
- Определите целевой элемент. Укажите элемент, который будет реагировать на действие. Если у товара нет уникального значения атрибута
id, добавьте его. - Настройте кнопку вызова. Используйте атрибут
commandfor, чтобы указать наidцели, и атрибутcommand, чтобы указать название специальной команды (с префиксом--). - Обработка события команды. Прикрепите прослушиватель событий
commandнепосредственно к целевому элементу. Объект события содержит свойствоcommandи свойствоtarget(ссылающееся на элемент, идентифицированныйcommandfor). - Обработка состояний ARIA. Специальные команды не имеют встроенной семантики, поэтому вам нужно обрабатывать такие состояния, как
aria-pressedилиaria-expanded.
Пример: пользовательские элементы управления анимацией
<!-- 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>
Основные ограничения
- Префикс специальных команд. ОБЯЗАТЕЛЬНО: все названия специальных команд должны начинаться с
--(например,command="--my-action"). - Таргетинг. Атрибут
commandforдолжен соответствовать атрибутуidэлемента в том же дереве документа. - Без всплытия. Событие
commandне всплывает. Если возможных целей несколько, добавьте{ capture: true }в обработчик событий и прослушивайте родительский элемент. - Корневые элементы Shadow DOM. Если целевой элемент может находиться в корневом элементе Shadow DOM, используйте
event.composedPath()[0]вместоevent.target. - Специальные возможности. Специальные команды не имеют встроенной семантики, и вы должны явно применять любые состояния.
Специальные возможности
- Если кнопка показывает дополнительный контент, используйте атрибут
aria-expanded, по умолчанию заданный какaria-expanded="false"иaria-expanded="true", когда показывается сопутствующий контент. - Если кнопка сама по себе выполняет действие (например, переключатель, который может воспроизводить и приостанавливать, кнопки регулировки громкости, которые могут отключать и включать звук), используйте атрибут
aria-pressed, задав для него значениеaria-pressed="false", когда он неактивен, иaria-pressed="true", когда он активен. - Если кнопка открывает элемент с ролью
menu,listbox,tree,gridилиdialog, то у элемента управления должен быть атрибутaria-haspopup, значение которого соответствует роли открываемого элемента. - Если кнопка отключается при активации, вместо
disabledиспользуйтеaria-disabled="true", чтобы не потерять фокус.
Резервные стратегии
Если Invoker Commands API не поддерживается, событие command не будет активировано. Чтобы обеспечить полную поддержку во всех современных браузерах, рекомендуется использовать invokers-polyfill с сайта https://github.com/keithamus/invokers-polyfill через npm install или CDN.
Этот полифил полностью поддерживает специальные действия (начиная с --) и отправляет событие command точно так же, как нативный API.
Динамический импорт (оптимизация производительности)
Для оптимальной производительности загружайте полифил, только если браузер не поддерживает API. Это позволяет экономить трафик и сокращает время выполнения скриптов в современных браузерах.
Примечание. Этот полифил не управляет состояниями ARIA (такими как aria-pressed или aria-expanded) для пользовательских команд. Чтобы обеспечить доступность сайта, необходимо вручную синхронизировать эти состояния в прослушивателе событий.
// 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);
}
});
Резервное объявление, заданное вручную (традиционный шаблон)
Если вы не хотите использовать полифил, то можете отправлять события с помощью делегирования событий и обрабатывать действия с помощью реестра команд. Это распространенный архитектурный шаблон в традиционной разработке на JavaScript, который остается высокоэффективным и масштабируемым.
// 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);
}
});