Invoker Commands: Additional Ways To Work With Dialog, Popover… And More?

Invoker Commands: Additional Ways To Work With Dialog, Popover… And More?

Invoker Commands: Additional Ways To Work With Dialog, Popover… And More?


The Popover API and <dialog> element are two of my favorite new platform features. In fact, I recently [wrote a detailed overview of their use cases] and the sorts of things you can do with them, even learning a few tricks in the process that I couldn’t find documented anywhere else.

I’ll admit that one thing that I really dislike about popovers and dialogs is that they could’ve easily been combined into a single API. They cover different use cases (notably, dialogs are typically modal) but are quite similar in practice, and yet their implementations are different.

Well, web browsers are now experimenting with two HTML attributes — technically, they’re called “invoker commands” — that are designed to invoke popovers, dialogs, and further down the line, all kinds of actions without writing JavaScript. Although, if you do reach for JavaScript, the new attributes — command and commandfor — come with some new events that we can listen for.

Invoker commands? I’m sure you have questions, so let’s dive in.

We’re in experimental territory

Before we get into the weeds, we’re dealing with experimental features. To use invoker commands today in November 2024 you’ll need Chrome Canary 134+ with the enable-experimental-web-platform-features flag set to Enabled, Firefox Nightly 135+ with the dom.element.invokers.enabled flag set to true, or Safari Technology Preview with the InvokerAttributesEnabled flag set to true.

I’m optimistic we’ll get baseline coverage for command and commandfor in due time considering how nicely they abstract the kind of work that currently takes a hefty amount of scripting.

Basic command and commandfor usage

First, you’ll need a <button> or a button-esque <input> along the lines of <input type="button"> or <input type="reset">. Next, tack on the command attribute. The command value should be the command name that you want the button to invoke (e.g., show-modal). After that, drop the commandfor attribute in there referencing the dialog or popover you’re targeting by its id.

<button command="show-modal" commandfor="dialogA">Show dialogA</button>

<dialog id="dialogA">...</dialog>

In this example, I have a <button> element with a command attribute set to show-modal and a commandfor attribute set to dialogA, which matches the id of a <dialog> element we’re targeting:

Let’s get into the possible values for these invoker commands and dissect what they’re doing.

Looking closer at the attribute values

The show-modal value is the command that I just showed you in that last example. Specifically, it’s the HTML-invoked equivalent of JavaScript’s showModal() method.

The main benefit is that show-modal enables us to, well… show a modal without reaching directly for JavaScript. Yes, this is almost identical to how HTML-invoked popovers already work with thepopovertarget and popovertargetaction attributes, so it’s cool that the “balance is being redressed” as the Open UI explainer describes it, even more so because you can use the command and commandfor invoker commands for popovers too.

There isn’t a show command to invoke show() for creating non-modal dialogs. I’ve mentioned before that non-modal dialogs are redundant now that we have the Popover API, especially since popovers have ::backdrops and other dialog-like features. My bold prediction is that non-modal dialogs will be quietly phased out over time.

The close command is the HTML-invoked equivalent of JavaScript’s close() method used for closing the dialog. You probably could have guessed that based on the name alone!

<dialog id="dialogA">
  <!-- Close #dialogA -->
  <button command="close" commandfor="dialogA">Close dialogA</button>
</dialog>

The show-popover, hide-popover, and toggle-popover values

<button command="show-popover" commandfor="id">

…invokes showPopover(), and is the same thing as:

<button popovertargetaction="show" popovertarget="id">

Similarly:

<button command="hide-popover" commandfor="id">

…invokes hidePopover(), and is the same thing as:

<button popovertargetaction="hide" popovertarget="id">

Finally:

<button command="toggle-popover" commandfor="id">

…invokes togglePopover(), and is the same thing as:

<button popovertargetaction="toggle" popovertarget="id">
<!--  or <button popovertarget="id">, since ‘toggle’ is the default action anyway. -->

I know all of this can be tough to organize in your mind’s eye, so perhaps a table will help tie things together:

command Invokes popovertargetaction equivalent
show-popover showPopover() show
hide-popover hidePopover() hide
toggle-popover togglePopover() toggle

So… yeah, popovers can already be invoked using HTML attributes, making command and commandfor not all that useful in this context. But like I said, invoker commands also come with some useful JavaScript stuff, so let’s dive into all of that.

Listening to commands with JavaScript

Invoker commands dispatch a command event to the target whenever their source button is clicked on, which we can listen for and work with in JavaScript. This isn’t required for a <dialog> element’s close event, or a popover attribute’s toggle or beforetoggle event, because we can already listen for those, right?

For example, the Dialog API doesn’t dispatch an event when a <dialog> is shown. So, let’s use invoker commands to listen for the command event instead, and then read event.command to take the appropriate action.

// Select all dialogs
const dialogs = document.querySelectorAll("dialog");

// Loop all dialogs
dialogs.forEach(dialog => {

  // Listen for close (as normal)
  dialog.addEventListener("close", () => {
    // Dialog was closed
  });

  // Listen for command
  dialog.addEventListener("command", event => {

    // If command is show-modal
    if (event.command == "show-modal") {
      // Dialog was shown (modally)
    }

    // Another way to listen for close
    else if (event.command == "close") {
      // Dialog was closed
    }

  });
});

So invoker commands give us additional ways to work with dialogs and popovers, and in some scenarios, they’ll be less verbose. In other scenarios though, they’ll be more verbose. Your approach should depend on what you need your dialogs and popovers to do.

For the sake of completeness, here’s an example for popovers, even though it’s largely the same:

// Select all popovers
const popovers = document.querySelectorAll("[popover]");

// Loop all popovers
popovers.forEach(popover => {

  // Listen for command
  popover.addEventListener("command", event => {

    // If command is show-popover
    if (event.command == "show-popover") {
      // Popover was shown
    }

    // If command is hide-popover
    else if (event.command == "hide-popover") {
      // Popover was hidden
    }

    // If command is toggle-popover
    else if (event.command == "toggle-popover") {
      // Popover was toggled
    }

  });
});

Being able to listen for show-popover and hide-popover is useful as we otherwise have to write a sort of “if opened, do this, else do that” logic from within a toggle or beforetoggle event listener or toggle-popover conditional. But <dialog> elements? Yeah, those benefit more from the command and commandfor attributes than they do from this command JavaScript event.

Another thing that’s available to us via JavaScript is event.source, which is the button that invokes the popover or <dialog>:

if (event.command == "toggle-popover") {
  // Toggle the invoker’s class
  event.source.classList.toggle("active");
}

You can also set the command and commandfor attributes using JavaScript:

const button = document.querySelector("button");
const dialog = document.querySelector("dialog");

button.command = "show-modal";
button.commandForElement = dialog; /* Not dialog.id */

…which is only slightly less verbose than:

button.command = "show-modal";
button.setAttribute("commandfor", dialog.id);

Creating custom commands

The command attribute also accepts custom commands prefixed with two dashes (--). I suppose this makes them like CSS custom properties but for JavaScript events and event handler HTML attributes. The latter observation is maybe a bit (or definitely a lot) controversial since using event handler HTML attributes is considered bad practice. But let’s take a look at that anyway, shall we?

Custom commands look like this:

<button command="--spin-me-a-bit" commandfor="record">Spin me a bit</button>
<button command="--spin-me-a-lot" commandfor="record">Spin me a lot</button>
<button command="--spin-me-right-round" commandfor="record">Spin me right round</button>
const record = document.querySelector("#record");

record.addEventListener("command", event => {
  if (event.command == "--spin-me-a-bit") {
    record.style.rotate = "90deg";
  } else if (event.command == "--spin-me-a-lot") {
    record.style.rotate = "180deg";
  } else if (event.command == "--spin-me-right-round") {
    record.style.rotate = "360deg";
  }
});

event.command must match the string with the dashed (--) prefix.

Are popover and <dialog> the only features that support invoker commands?

According to Open UI, invokers targeting additional elements such as <details> were deferred from the initial release. I think this is because HTML-invoked dialogs and an API that unifies dialogs and popovers is a must-have, whereas other commands (even custom commands) feel more like a nice-to-have deal.

However, based on experimentation (I couldn’t help myself!) web browsers have actually implemented additional invokers to varying degrees. For example, <details> commands work as expected whereas <select> commands match event.command (e.g., show-picker) but fail to actually invoke the method (showPicker()). I missed all of this at first because MDN only mentions dialog and popover.

Open UI also alludes to commands for <input type="file">, <input type="number">, <video>, <audio>, and fullscreen-related methods, but I don’t think that anything is certain at this point.

So, what would be the benefits of invoker commands?

Well, a whole lot less JavaScript for one, especially if more invoker commands are implemented over time. Additionally, we can listen for these commands almost as if they were JavaScript events. But if nothing else, invoker commands simply provide more ways to interact with APIs such as the Dialog and Popover APIs. In a nutshell, it seems like a lot of “dotting i’s” and “crossing-t’s” which is never a bad thing.

Leave a Reply

Your email address will not be published. Required fields are marked *

Secured By miniOrange