Memperkenalkan command dan commandfor

Dipublikasikan: 7 Maret 2025

Tombol sangat penting untuk membuat aplikasi web dinamis. Tombol membuka menu, mengaktifkan tindakan, dan mengirimkan formulir. API ini menyediakan fondasi interaktivitas di web. Membuat tombol sederhana dan mudah diakses dapat menyebabkan beberapa tantangan yang mengejutkan. Developer yang membuat frontend mikro atau sistem komponen dapat menemukan solusi yang menjadi lebih kompleks dari yang diperlukan. Meskipun framework membantu, web dapat melakukan lebih banyak hal di sini.

Chrome 135 memperkenalkan kemampuan baru untuk memberikan perilaku deklaratif dengan atribut command dan commandfor baru, yang meningkatkan dan mengganti atribut popovertargetaction dan popovertarget. Atribut baru ini dapat ditambahkan ke tombol, sehingga browser dapat mengatasi beberapa masalah inti terkait kesederhanaan dan aksesibilitas, serta memberikan fungsi umum bawaan.

Pola tradisional

Membuat perilaku tombol tanpa framework dapat menimbulkan beberapa tantangan menarik seiring berkembangnya kode produksi. Meskipun HTML menawarkan pengendali onclick ke tombol, hal ini sering kali tidak diizinkan di luar demo atau tutorial karena aturan Kebijakan Keamanan Konten (CSP). Meskipun peristiwa ini dikirimkan pada elemen tombol, tombol biasanya ditempatkan di halaman untuk mengontrol elemen lain yang memerlukan kode untuk mengontrol dua elemen sekaligus. Anda juga perlu memastikan interaksi ini dapat diakses oleh pengguna teknologi pendukung. Hal ini sering kali menyebabkan kode terlihat seperti ini:

<div class="menu-wrapper">
  <button class="menu-opener" aria-expanded="false">
    Open Menu
  </button>
  <div popover class="menu-content">
    <!-- ... -->
  </div>
</div>
<script type="module">
document.addEventListener('click', e => {  
  const button = e.target;
  if (button.matches('.menu-opener')) {
    const menu = button
      .closest('.menu-wrapper')
      .querySelector('.menu-content');
    if (menu) {
      button.setAttribute('aria-expanded', 'true');
      menu.showPopover();
      menu.addEventListener('toggle', e => {
        // reset back to aria-expanded=false on close
        if (e.newState == 'closed') {
          button.setAttribute('aria-expanded', 'false');
        }
      }, {once: true})
    }
  }
});
</script>

Pendekatan ini dapat sedikit rapuh, dan framework bertujuan untuk meningkatkan ergonomi. Pola umum dengan framework seperti React mungkin melibatkan pemetaan klik ke perubahan status:

function MyMenu({ children }) {
  const [isOpen, setIsOpen] = useState(false);
  const open = useCallback(() => setIsOpen(true), []);
  const handleToggle = useCallback((e) => {
      // popovers have light dismiss which influences our state
     setIsOpen(e.newState === 'open')
  }, []);
  const popoverRef = useRef(null);
  useEffect(() => {
    if (popoverRef.current) {
      if (isOpen) {
        popoverRef.current.showPopover();
      } else {
        popoverRef.current.hidePopover();
      }
    }
  }, [popoverRef, isOpen]);
  return (
    <>
      <button onClick={open} aria-expanded={isOpen}>
        Open Menu
      </button>
      <div popover onToggle={handleToggle} ref={popoverRef}>
        {children}
      </div>
    </>
  );
}

Banyak framework lain juga bertujuan untuk memberikan ergonomi yang serupa, misalnya ini dapat ditulis di AlpineJS sebagai:

<div x-data="{open: false}">
  <button @click="open = !open; $refs.popover.showPopover()" :aria-expanded="open">
    Open Menu
  </button>
  <div popover x-ref="popover" @toggle="open = $event.newState === 'open'">
    <!-- ... -->
  </div>
</div>

Saat menulis ini di Svelte, tampilannya mungkin seperti ini:

<script>
  let popover;
  let open = false;
  function togglePopover() {
    open ? popover.hidePopover() : popover.showPopover();
    open = !open;
  }
</script>
<button on:click={togglePopover} aria-expanded={open}>
  Open Menu
</button>
<div bind:this={popover} popover>
  <!-- ... -->
</div>

Beberapa sistem atau library desain mungkin melangkah lebih jauh, dengan menyediakan wrapper di sekitar elemen tombol yang mengenkapsulasi perubahan status. Hal ini memisahkan perubahan status di balik komponen pemicu, dengan mengorbankan sedikit fleksibilitas untuk meningkatkan ergonomi:

import {MenuTrigger, MenuContent} from 'my-design-system';
function MyMenu({children}) {
  return (
    <MenuTrigger>
      <button>Open Menu</button>
    </MenuTrigger>
    <MenuContent>{children}</MenuContent>
  );
}

Pola command dan commandfor

Dengan atribut command dan commandfor, tombol kini dapat melakukan tindakan pada elemen lain secara deklaratif, sehingga memberikan ergonomi framework tanpa menyedekatkan fleksibilitas. Tombol commandfor menggunakan ID—mirip dengan atribut for—sedangkan command menerima nilai bawaan, sehingga memungkinkan pendekatan yang lebih portabel dan intuitif.

Contoh: Tombol menu terbuka dengan command dan commandfor

HTML berikut menyiapkan hubungan deklaratif antara tombol dan menu yang memungkinkan browser menangani logika dan aksesibilitas untuk Anda. Anda tidak perlu mengelola aria-expanded atau menambahkan JavaScript tambahan.

<button commandfor="my-menu" command="show-popover">
Open Menu
</button>
<div popover id="my-menu">
  <!-- ... -->
</div>

Membandingkan command dan commandfor dengan popovertargetaction dan popovertarget

Jika pernah menggunakan popover sebelumnya, Anda mungkin sudah memahami atribut popovertarget dan popovertargetaction. Fungsi ini berfungsi mirip dengan commandfor dan command—kecuali fungsi ini khusus untuk popover. Atribut command dan commandfor sepenuhnya menggantikan atribut lama ini. Atribut baru mendukung semua yang dilakukan atribut lama, serta menambahkan kemampuan baru.

Perintah bawaan

Atribut command memiliki serangkaian perilaku bawaan yang dipetakan ke berbagai API untuk elemen interaktif:

  • show-popover: Dipetakan ke el.showPopover().
  • hide-popover: Dipetakan ke el.hidePopover().
  • toggle-popover: Dipetakan ke el.togglePopover().
  • show-modal: Dipetakan ke dialogEl.showModal().
  • close: Dipetakan ke dialogEl.close().

Perintah ini dipetakan ke pasangan JavaScript-nya, sekaligus menyederhanakan aksesibilitas (seperti memberikan hubungan yang setara dengan aria-details dan aria-expanded), manajemen fokus, dan lainnya.

Contoh: Dialog konfirmasi dengan command dan commandfor

<button commandfor="confirm-dialog" command="show-modal">
  Delete Record
</button>
<dialog id="confirm-dialog">
  <header>
    <h1>Delete Record?</h1>
    <button commandfor="confirm-dialog" command="close" aria-label="Close" value="close">
      <img role="none" src="/close-icon.svg">
    </button>
  </header>
  <p>Are you sure? This action cannot be undone</p>
  <footer>
    <button commandfor="confirm-dialog" command="close" value="cancel">
      Cancel
    </button>
    <button commandfor="confirm-dialog" command="close" value="delete">
      Delete
    </button>
  </footer>
</dialog>

Mengklik tombol Delete Record akan membuka dialog sebagai modal, sedangkan mengklik tombol Close, Cancel, atau Delete akan menutup dialog sekaligus mengirim peristiwa "close" pada dialog, yang memiliki properti returnValue yang cocok dengan nilai tombol. Hal ini mengurangi kebutuhan JavaScript di luar satu pemroses peristiwa di dialog untuk menentukan tindakan selanjutnya:

dialog.addEventListener("close", (event) => {
  if (event.target.returnValue == "cancel") {
    console.log("cancel was clicked");
  } else if (event.target.returnValue == "close") {
    console.log("close was clicked");
  } else if (event.target.returnValue == "delete") {
    console.log("delete was clicked");
  }
});

Perintah kustom

Selain perintah bawaan, Anda dapat menentukan perintah kustom menggunakan awalan --. Perintah kustom akan mengirim peristiwa "command" pada elemen target (seperti perintah bawaan), tetapi tidak akan pernah melakukan logika tambahan seperti yang dilakukan nilai bawaan. Hal ini memberikan fleksibilitas untuk mem-build komponen yang dapat bereaksi terhadap tombol dengan berbagai cara tanpa harus menyediakan komponen wrapper, menjelajahi DOM untuk elemen target, atau memetakan klik tombol ke perubahan status. Hal ini memungkinkan Anda menyediakan API dalam HTML untuk komponen Anda:

<button commandfor="the-image" command="--rotate-landscape">
 Landscape
</button>
<button commandfor="the-image" command="--rotate-portrait">
 Portrait
</button>

<img id="the-image" src="photo.jpg">

<script type="module">
  const image = document.getElementById("the-image");
  image.addEventListener("command", (event) => {
   if ( event.command == "--rotate-landscape" ) {
    image.style.rotate = "-90deg"
   } else if ( event.command == "--rotate-portrait" ) {
    image.style.rotate = "0deg"
   }
  });
</script>

Perintah di ShadowDOM

Mengingat atribut commandfor menggunakan ID, ada batasan terkait melintasi DOM bayangan. Dalam hal ini, Anda dapat menggunakan JavaScript API untuk menetapkan properti .commandForElement yang dapat menetapkan elemen apa pun, di seluruh root bayangan:

<my-element>
  <template shadowrootmode=open>
    <button command="show-popover">Show popover</button>
    <slot></slot>
  </template>
  <div popover><!-- ... --></div>
</my-element>
<script>
customElements.define("my-element", class extends HTMLElement {
  connectedCallback() {
    const popover = this.querySelector('[popover]');
    // The commandForElement can set cross-shadow root elements.
    this.shadowRoot.querySelector('button').commandForElement = popover;
  }
});
</script>

Proposal mendatang dapat memberikan cara deklaratif untuk membagikan referensi di seluruh batas bayangan, seperti Proposal Target Referensi.

Apa langkah selanjutnya?

Kami akan terus mempelajari kemungkinan untuk perintah bawaan baru, guna mencakup fungsi umum yang digunakan situs. Ide yang diusulkan dibahas dalam Proposal UI Terbuka. Beberapa ide yang telah dieksplorasi:

  • Membuka dan menutup elemen <details>.
  • Perintah "show-picker" untuk elemen <input> dan <select>, yang dipetakan ke showPicker().
  • Perintah pemutaran untuk elemen <video> dan <audio>.
  • Menyalin konten teks dari elemen.

Kami menerima masukan komunitas. Jika Anda memiliki saran, jangan ragu untuk melaporkan masalah di Open UI Issue Tracker.

Pelajari lebih lanjut

Temukan informasi selengkapnya tentang command dan commandfor di spesifikasi dan di MDN.