Thao tác tuỳ chỉnh bằng nút

Tính năng web: Lệnh của trình gọi

Invoker Commands API cho phép các nút kích hoạt hành động trên các phần tử mục tiêu theo cách khai báo bằng thuộc tính HTML. Cách tiếp cận này giúp giảm nhu cầu về trình nghe sự kiện thủ công và tách giao diện người dùng khỏi thông tin triển khai. Đối với các thao tác tuỳ chỉnh, dành riêng cho ứng dụng, bạn có thể xác định tên lệnh của riêng mình. Lệnh tuỳ chỉnh phải có tiền tố là dấu gạch ngang kép (--) để tránh xung đột với các lệnh tích hợp sẵn trong trình duyệt trong tương lai.

Các bước triển khai

  1. Xác định phần tử mục tiêu: Xác định phần tử sẽ phản hồi hành động. Nếu không có id riêng biệt, hãy thêm một mã.
  2. Định cấu hình nút trình gọi: Sử dụng thuộc tính commandfor để trỏ đến id của mục tiêu và thuộc tính command để chỉ định tên lệnh tuỳ chỉnh (có tiền tố --).
  3. Xử lý sự kiện lệnh: Đính kèm trình nghe sự kiện command trực tiếp vào phần tử mục tiêu. Đối tượng sự kiện chứa thuộc tính command và thuộc tính target (tham chiếu đến phần tử được xác định bằng commandfor).
  4. Xử lý trạng thái aria: Các lệnh tuỳ chỉnh không có ngữ nghĩa vốn có và bạn phải xử lý các trạng thái như aria-pressed hoặc aria-expanded.

Ví dụ: Các chế độ điều khiển ảnh động tuỳ chỉnh

<!-- 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>

Các quy tắc ràng buộc chính

  • Tiền tố của lệnh tuỳ chỉnh: BẮT BUỘC: Tất cả tên lệnh tuỳ chỉnh phải bắt đầu bằng -- (ví dụ: command="--my-action").
  • Nhắm mục tiêu: Thuộc tính commandfor phải khớp với id của một phần tử trong cùng một cây tài liệu.
  • Không nổi bọt: Sự kiện command không nổi bọt. Nếu có nhiều mục tiêu, hãy thêm { capture: true } vào trình xử lý sự kiện và theo dõi một đối tượng cấp trên.
  • Gốc bóng: Nếu mục tiêu có thể nằm trong gốc bóng, hãy sử dụng event.composedPath()[0] thay vì event.target.
  • Hỗ trợ tiếp cận: Các lệnh tuỳ chỉnh không có ngữ nghĩa vốn có và bạn phải áp dụng rõ ràng mọi trạng thái.

Những điểm cần lưu ý về khả năng hỗ trợ tiếp cận

  • Nếu nút này hiển thị nội dung bổ sung, hãy sử dụng thuộc tính aria-expanded, đặt thành aria-expanded="false" theo mặc định và aria-expanded="true" khi nội dung đi kèm được hiển thị
  • Nếu nút này hỗ trợ một hoạt động tương tác (ví dụ: nút bật/tắt có thể phát và tạm dừng, nút âm lượng có thể tắt tiếng và bật tiếng), hãy sử dụng thuộc tính aria-pressed, đặt thành aria-pressed="false" khi không hoạt động và aria-pressed="true" khi hoạt động
  • Nếu nút mở một phần tử có vai trò là menu, listbox, tree, grid hoặc dialog, thì thành phần điều khiển phải có thuộc tính aria-haspopup, với giá trị được đặt thành vai trò của phần tử mà nút đó mở
  • Nếu nút bị tắt khi kích hoạt, hãy sử dụng aria-disabled="true" thay vì disabled để không bị mất tiêu điểm

Chiến lược dự phòng

Nếu API Lệnh của trình gọi không được hỗ trợ, thì sự kiện command sẽ không kích hoạt. Để được hỗ trợ đầy đủ trên tất cả trình duyệt hiện đại, bạn nên sử dụng invokers-polyfill từ https://github.com/keithamus/invokers-polyfill thông qua npm install hoặc CDN.

Polyfill này hỗ trợ đầy đủ các thao tác tuỳ chỉnh (bắt đầu bằng --) và gửi sự kiện command giống hệt như API gốc.

Nhập động (Tối ưu hoá hiệu suất)

Để có hiệu suất tốt nhất, bạn chỉ nên tải polyfill nếu trình duyệt không hỗ trợ API gốc. Điều này giúp tiết kiệm băng thông và giảm thời gian thực thi tập lệnh cho người dùng trên các trình duyệt hiện đại.

LƯU Ý: Polyfill này không quản lý các trạng thái ARIA (như aria-pressed hoặc aria-expanded) cho các lệnh tuỳ chỉnh. Bạn phải đồng bộ hoá các trạng thái này theo cách thủ công trong trình nghe sự kiện để đảm bảo trang web của bạn có thể truy cập được.

// 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);
  }
});

Dự phòng thủ công (Mẫu truyền thống)

Nếu không muốn sử dụng polyfill, bạn có thể kết hợp uỷ quyền sự kiện để gửi sự kiện và sổ đăng ký lệnh để xử lý các hành động. Đây là một mẫu kiến trúc phổ biến trong quá trình phát triển JavaScript truyền thống, vẫn mang lại hiệu quả cao và có khả năng mở rộng.

// 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);
  }
 });