Invoker Commands (Explainer)
- Authors: Keith Cirkel
Invoker Buttons
Pitch in Code
<button command="show-modal" commandfor="my-dialog">This opens a dialog</button>
<dialog id="my-dialog">This is the dialog</dialog>
Introduction
Adding commandfor
and command
attributes to <button>
and elements would allow authors to assign behaviour to buttons in a more accessible and declarative way, while reducing bugs and simplifying the amount of JavaScript pages are required to ship for interactivity. Buttons with command
will - when clicked, touched, or enacted via keypress - dispatch a CommandEvent
on the element referenced by commandfor
, with some default behaviours.
Background
All elements within the DOM are capable of having interactions added to them. A long while ago this took the form of adding inline JavaScript to an event attribute, such as <button onclick="other.open()"></button>
. Inline JavaScript has (rightly so) fallen out of favour due to the security and maintainability concerns. Newer pages may instead introduce more JavaScript to imperatively discover elements and call addEventListener('click', ...)
to invoke the same behaviour. These patterns reduce developer experience and introduce more boilerplate and friction, while remediating security and maintainability concerns. Some frameworks attempt to reintroduce the developer experience of inline handlers by introducing new JavaScript or HTML shorthands, such as React’s onClick={...}
, Vue’s @click=".."
or HTMX’s hx-trigger="click"
.
Handling clicking is only half of the problem though, and this proposal doesn’t seek to re-invent click handlers. Dispatching an action to a related element on the click can also become chore-like depending on the framework of choice. Many frameworks recommend associating a click event with a state change and having the element to be invoked react to that state change. For example the React docs on Adding Interactivity attach click handlers which set state.
There has also been a rise in the desire to customise controls for components. Many sites, for example, introduce custom controls for File Uploads or dropdown menus. These often require a large amount of work to reintroduce the built in functionality of those controls, and often unintentionally sacrifice accessibility in doing so.
With the new popover
attribute, we saw a straightforward declarative way to tell the DOM that a button was interested in being a participant of the popover interaction. popovertarget
would indicate to a browser that if that button was interacted with, it should open the element the popovertarget
value pointed to. This allows for popovers to be created and interacted with - in an accessible and reliable way - without writing any additional JavaScript, which is a very welcome addition. While popovertarget
sufficiently captured the use case for popovers, it fell short of providing the same developer & user experience for other interactive elements such as <dialog>
, <details>
, <video>
, <input type="file">
, and so on. This proposal attempts to redress the balance.
Terms
- Invoke/Invoked/Invoking: The action of Invoking refers to a complete press action of a button, using a Human Input Device (HID). If the HID is a mouse, this would be a
click
event. If the HID is a touch screen, this would be a press using a stylus or finger. If the HID is a keyboard this would be theSpace
orEnter
(Carriage Return) key on the keyboard. For other HIDs such as eye tracking or pedals or game controllers, the equivalent expected “click” action should be used to invoke the element. - Invoker: An invoker is a button element (that is a
<button>
,<input type="button">
, or<input type="reset">
) that has ancommandfor
andcommand
attribute set. - Invoker Target: An element which is referenced to by an Invoker, via the
commandfor
attribute.
Proposed Plan
In the style of popovertarget
, this document proposes we add commandForElement
, and command
as available attributes to <button>
.
interface mixin CommandElement {
[CEReactions] attribute Element? commandForElement;
[CEReactions] attribute DOMString command;
};
HTMLButtonElement extends InvokerElement
The commandfor
value should be an IDREF pointing to an element within the document. .commandForElement
also exists on the element to imperatively assign a node to be the invoker target, allowing for cross-root invokers (in some cases, see the popovertarget attr-asociated element steps for more).
The command
attribute (and the .command
reflected property) is a freeform hint to the invoker target. Command can be a “built-in” action or a “custom” action. If command
is not supplied then it is considered invalid and no action will take place, and no event will be fired.
Custom command values must start with a double dash (--
), similar to the CSS “dashed-ident” syntax. This is a safe namespace to use, and browsers guarantee to never have built-in values starting with --
. The only browser behaviour for those values will be dispatching a CommandEvent
on the invokee. This allows web developers to create custom Invoke actions for their components.
Built-in interactive elements have built-in behaviours (detailed below) which are determined by the command
attribue/property. The built-in names must not start with a -
.
Valid commands (that is: custom commands or a valid built-in) will dispatch CommandEvents, allowing custom code to take control of invocations (for example calling .preventDefault()
or executing custom side-effects).
Invokers will dispatch an CommandEvent
on the Invokee (the element referenced by commandfor
) when the element is Invoked. The CommandEvent
’s type
is always command
. The event also contains an source
property that will reference button that was enacted causing the CommandEvent
to fire. The dispatched CommandEvents
are always non-bubbling, composed, trusted, cancellable events.
[Exposed=Window]
interface CommandEvent : Event {
constructor(CommandEventInit invokeEventInit);
readonly attribute Element source;
readonly attribute DOMString type = "command";
readonly attribute DOMString command;
};
dictionary CommandEventInit : EventInit {
DOMString action = "";
Element source;
};
If an element also has both a popovertarget
and the commandfor
/command
attributes set, then popovertarget
must be ignored: command
takes precedence.
If a <button>
is a form participant, or has type=submit
, then command
must be ignored. It also must not submit the form it is attached to. Buttons like this are “author errors”, and browsers may issue warnings in the developer console explaining to the developer why this button doesn’t work.
This means a <button>
with command
/commandfor
that is inside a form must also have type=button
.
Example Code
Popovers
When pointing to a popover
, commandfor
acts much like popovertarget
, allowing the toggling of popovers.
<button commandfor="my-popover" command="toggle-popover">Open Popover</button>
<!-- Effectively the same as popovertarget="my-popover" -->
<div id="my-popover" popover="auto">Hello world</div>
Dialogs
When pointing to a <dialog>
, command
can toggle a <dialog>
’s openness.
<button commandfor="my-dialog" command="show-modal">Open Dialog</button>
<dialog id="my-dialog">
Hello world!
<button commandfor="my-dialog" command="close">Close</button>
</dialog>
Details
Note: Invokers targeting a
<details>
element has been deferred from the initial release.
When pointing to a <details>
, commandfor
can toggle a <details>
’ openness.
<button commandfor="my-details" command="open">Open Details</button>
<!-- Can be used to replicate the `<summary>` interaction -->
<details id="my-details">
<summary>Summary...</summary>
Hello world!
</details>
Customizing input type=file
Note: Invokers targeting a
<input>
element has been deferred from the initial release.
Pointing commandfor
to an <input type="file">
can act the same as the rendered button within the input; and can be used to declare a customised alternative button to the input’s button.
<button commandfor="my-file" command="show-picker">Pick a file...</button>
<input id="my-file" type="file" />
Customizing video/audio controls
Note: Invokers targeting
<audio>
and<video>
elements has been deferred from the initial release.
The <video>
and <audio>
tags have many interactions, here commandfor
shines, allowing multiple buttons to handle different interactions with the video player.
<button commandfor="my-video" command="play-pause">Play/Pause</button>
<button commandfor="my-video" command="toggle-muted">Mute/Unmute</button>
<video id="my-video"></video>
Custom behaviour
Invokers will dispatch events on the Invokee element. Using a dash in the command allows for custom JavaScript to be triggered without having to wire up manual event handlers to the Invokers.
<!-- Custom commands must contain a double dash! -->
<button commandfor="my-custom" command="--my-frobulate">Frobulate</button>
<div id="my-custom"></div>
<script>
document.getElementById("my-custom").addEventListener("command", (e) => {
if (e.action === "--my-frobulate") {
alert("Successfully frobulated the div");
}
});
</script>
Defaults
Depending on the target set by commandfor
, invoking the button will trigger additional behaviours alongside the event dispatch, depending on the value of command
. The following table represents how built-in invocations on specific element types are handled.
When the command
attribute is missing it will default to an invalid state.
Invokee Element | action hint | Behaviour |
---|---|---|
<* popover> | 'toggle-popover' | Shows the popover if closed, otherwise hides. Similar to .togglePopover() |
<* popover> | 'hide-popover' | Hides the popover if open, otherwise does nothing. Similar to .hidePopover() |
<* popover> | 'show-popover' | Shows the popover if closes, otherwise does nothing. Similar to .showPopover() |
<dialog> | 'show-modal' | If the <dialog> is not open , shows it as modal. Similar to .showModal() |
<dialog> | 'close' | If the <dialog> is open , close and use the button value for returnValue. Similar to `.close(value) |
Further behaviours have been proposed, but need further design on exactly how they will behave based on implications such as accessibility, security, interactivity, and how the button may need to respond to such actions.
Invokee Element | action hint | Behaviour |
---|---|---|
<details> | 'toggle' | If the <details> is open , then close it, otherwise open it |
<details> | 'open' | If the <details> is not open , then open it |
<details> | 'close' | If the <details> is open , then close it |
<dialog> | 'toggle' | If the <dialog> is open , then close it and use the button value for returnValue, otherwise open as modal |
<dialog> | 'cancel' | If the <dialog> is open , cancel the dialog |
<select> | 'show-picker' | Shows the native picker. Similar to .showPicker() on the invokee |
<input> | 'show-picker' | Shows the native picker. Similar to .showPicker() on the invokee |
<video> | 'play-pause' | If the video is not playing, plays the video. Otherwise pauses it. Similar to el.playing = !el.playing |
<video> | 'pause' | If the video is playing, pause the video. Similar .playing = false |
<video> | 'play' | If the video is not playing, play the video. Similar to .playing = true |
<video> | 'toggle-muted' | If the video is muted, it unmutes the video, otherwise it mutes it. Similar to el.muted = !el.muted |
<audio> | 'play-pause' | If the audio is not playing, plays the audio. Otherwise pauses it. Similar to el.playing = !el.playing |
<audio> | 'pause' | If the audio is playing, pause the audio. Similar .playing = false |
<audio> | 'play' | If the audio is not playing, play the audio. Similar to .playing = true |
<audio> | 'toggle-muted' | If the audio is muted, it unmutes the audio, otherwise it mutes it. Similar to el.muted = !el.muted |
<*> | 'toggle-fullscreen' | If the element is fullscreen, then exit, otherwise request to enter |
<*> | 'request-fullscreen' | Request the element to enter into ‘fullscreen’ mode |
<*> | 'exit-fullscreen' | If the element is fullscreen, then exit |
<input type=number> | 'step-up' | Call .stepUp() on the invokee |
<input type=number> | 'step-down' | Call .stepDown() on the invokee |
Note: The above tables are an attempt at wide coverage, but ideas are welcome. Please submit a PR if you have one!
Further to the initial ship we’re also exploring implicit command
or implicit commandfor
values where the value can easily be inferred.
Accessibility
For built-in behaviours command
attribute maps to specific accessibility mappings which are placed on the button. The button may also use the commandfor
referenced element to gather other details (for example the state of the element) to reflect that state on the button.
These mappings will happen implicitly on the browsers Accessible Nodes, and so while (for simplicity) this secion refers to various aria-
attributes, buttons will not sprout these attributes in the DOM, but the effective equivalent will be exposed to Assistive Technologies.
Buttons with command=toggle-popover
, command=hide-popover
, or command=show-popover
Buttons with these popover commands will implicitly receive aria-details=IDREF
, where IDREF
matches that of the commandfor
attribute, under certain conditions while the popover is in the showing state.
Why?
Some Assistive Technologes allow users to press a keyboard shortcut to navigate to the element pointed to by aria-details
. This is useful in scenarios where the popover is not the invoking element’s next accessibility sibling, such as if there are buttons or other controls between the invoking button and the popover.
Browsers are expected to omit aria-details
based on heuristics where it is redundant, such as the contents of the popover being the next accessibility sibling to the invoking element, or while the popover’s contents are not traversable, such as when it is hidden, or not in the DOM.
Browsers should also omit aria-details
when the invoking button is a descendant of the popover it refers to, for example a close button inside the popover.
Buttons will also implicitly receive an aria-expanded
value. The state of the aria-expanded
value will map to the state of the popover. When the popover is open the button will have aria-expanded="true"
, when closed, aria-expanded="false"
. If the button is an descendant of the popover then it will be explictly set to aria-expanded="undefined"
, indicating to assistive technology that the expanded state is not applicable to this button. This attribute will be recomputed whenever the popover changes state, such that the button always reflects the state of expansion.
Why?
Some Assistive Technologies will announce the state of expansion while the button is focussed, for example a <button aria-expanded=false>File</button>
may be announced by a screen reader as “File, button, collapsed”, while <button aria-expanded=true>File</button>
may be announced by a screen reader as File, button, expanded
. This is helpful information for users who require an alternative of the visual confirmation of the popover being rendered on screen.
If the popover element is not in the DOM, browsers should ensure the button has the aria-expanded=false
state. Some websites may chose to render such elements only when they are open, often referred to as “conditional rendering”. Browsers should aim to do their best to cater for this, and as such provide the state under the assumpsion that a popover element may appear in the DOM on press.
Buttons with these popover commands will also have focus restored to the button when the popover closes. This will require the popover keeping a reference to the opening button so that it can return the focus to that button.
Why?
It is important for keyboard users (or other users who rely on focus) to not have their focus state lost when performing actions on the page. If the page shows a new piece of navigable UI, and shifts the user’s focus to that UI, then it is important to restore focus when that UI closes, so that the user’s journey is maintained.
Other considerations not explicitly proposed.
aria-pressed
Given elements will have aria-expanded
, adding aria-pressed
would be confusing or redundant, and as such won’t be proposed for these buttons.
aria-controls
While aria-controls
attempts to establish a similar style of relationship to aria-details
, aria-details
sees broader support among various assistive technologies, and it would be redundant to add both.
aria-haspopup=dialog
When the commandfor=
element is a <dialog>
, adding aria-haspopup=dialog is a potentially viable attribute to add. Some Assistive Technologies will announce that the button will open a dialog, for example <button aria-haspopup=dialog>Delete</button>
may be announced by a screen reader as “Delete, button, opens dialog”. However, buttons that open dialogs rarely come with additional visual treatment, and so the lack of a visual analog means that using this attribute likely provides surplus information to users of assistive technologies that isn’t otherwise provided. Additionally, the intent for aria-haspopup
is more focused towards combobox popovers, where the type of popover may be ambigious. As command buttons can be used for more than just combobox controls, aria-haspopup
is not present by default.
Buttons with command=show-modal
Buttons with this dialog command will also have focus restored to the button when the dialog closes. This will require the dialog keeping a reference to the opening button so that it can return the focus to that button.
Why?
It is important for keyboard users (or other users who rely on focus) to not have their focus state lost when performing actions on the page. If the page shows a new piece of navigable UI, and shifts the user’s focus to that UI, then it is important to restore focus when that UI closes, so that the user’s journey is maintained.
Other considerations not explicitly proposed.
aria-details
Exposing an aria-details
relationship between the button and the dialog is redundant. These relationships are useful only when the related element is open and present on the page and the invoking button is still navigable. A modal dialog renders the rest of the page inert meaning that it won’t be possible to navigate to the invoking button.
aria-controls
Similar to aria-details
, aria-controls
is not useful for a dialog that is closed, and non-navigable for a dialog that is open as modal, so it would provide no benefit to add.
aria-pressed
The aria-pressed
state is the incorrect type of association for elements which conditionally appear on the page.
aria-expanded
Similar to aria-details
, the aria-expanded
state redundant, given the dialog is opened as modal. For the user to have navigated to a button that opens a modal dialog, the dialog must not be open. If the dialog is open then the button will be inert, therefore non-navigable, therefore it is redundant to provide it an implicit aria-expanded=true
state also.
aria-haspopup=dialog
When the commandfor=
element is a <dialog>
it might seem aria-haspopup=dialog
would be a potentially viable attribute to add. Some Assistive Technologies will announce that the button will open a dialog, for example <button aria-haspopup=dialog>Delete</button>
may be announced by a screen reader as “Delete, button, opens dialog”. However, buttons that open dialogs rarely come with additional visual treatment, and so the lack of a visual analog means that using this attribute likely provides surplus information to users of assistive technologies that isn’t otherwise provided. Additionally, the intent for aria-haspopup
is more focused towards combobox popovers, where the type of popover may be ambigious. As command buttons can be used for more than just combobox controls, aria-haspopup
is not present by default.
Buttons with command=close
Buttons with this dialog command will implicitly receive aria-details=IDREF
, where IDREF
matches that of the commandfor
attribute, while the dialog is in the showing state, and the button is not a descendant of the dialog.
Why?
A button that closes a dialog is very typically found inside of the dialog, but in some cases it may be found outside, perhaps as a “toggle” style button which opens and closes a dialog as non-modal. This button may be used to close an open dialog that is shown as non-modal. It may be useful for a user to traverse into the dialog before closing it, for example to check if they have unsaved changes within the dialog.
Buttons will also implicitly receive an aria-expanded
value, if they are not a descendant of the commandfor=
referenced element. The state of the aria-expanded
value will map to the state of the dialog’s openness. When the dialog is open the button will have aria-expanded="true"
, when closed, aria-expanded="false"
. This will be recomputed whenever the dialog changes state, such that the button always reflects the state of openness.
Why?
A button that closes a dialog is very typically found inside of the dialog, but in some cases it may be found outside, perhaps as a “toggle” style button which opens and closes a dialog as non-modal.
Buttons outside of the dialog may be used to close an open dialog that is shown as non-modal. It may be useful for a user to traverse into the dialog before closing it, for example to check if they have unsaved changes within the dialog. It may also be useful to know if the dialog is already closed (as in its aria-expanded
state is false), as this may help the user make a decision to whether or not they action the close button.
Other considerations not explicitly proposed.
aria-pressed
Given elements will have aria-expanded
, adding aria-pressed
would be confusing or redundant, and as such won’t be proposed for these buttons.
aria-controls
While aria-controls
attempts to establish a similar style of relationship to aria-details
, aria-details
sees broader support among various assistive technologies, and it would be redundant to add both.
Other built-in command=
types
The above commands are the core built-in command types which will be initially shipping. Further built-in commands will be proposed on a case-per-case basis, and additional aria or other logic will be considered with those at the time.
Custom command=
types
Custom command types have accessibility requirements which will be hard to determine based on the presence of the button and invokee alone. Consequently, it is left to the implementer or user of the custom command to assign appropriate aria markup to these buttons.
Elements with commandfor=
but no command=
In this current proposal, elements without a command=
attribute are considered “author errors”; they will do nothing when invoked. These buttons will recieve no additional implicit aria states.
Invokers and Custom Elements
As the underlying system for invoke elements uses event dispatch, Custom Elements can make use of CommandEvent
for their own behaviours. Consider the following:
<button commandfor="my-element" command="--spin-widget">
Spin the widget
</button>
<spin-widget id="my-element"></spin-widget>
<script>
customElements.define(
"spin-widget",
class extends HTMLElement {
connectedCallback() {
this.addEventListener("command", (e) => {
if (e.action === "--spin-widget") {
this.spin();
}
});
}
},
);
</script>
Security
This proposal adds new capabilities in that elements can be interacted with using HTML where prior these required scripting (read: JS). Care has been taken within this proposal to ensure there are no substantially new security implications, and that the risk is kept low.
In general, we should not grant significant new capabilities with or without scripting, and seek other ways to mitigate security concerns.
Invokers, by design, have two security affordances:
- Without scripting, they require user interaction - the invoker button needs to be clicked for the browser to fire an Invoke Event. Scripts are able to synthesise the
InvokeEvent
, or theclick
event on the button - but scripts are already capable of causing the same interactions without Invokers. - The
invoketarget
takes an IDREF which must reference an ID within the same document.invokeTargetElement
can be assigned a cross-root element, but this requires scripting which means all actions are already possible.
To go into more detail, below is a brief summary of the security considerations per interaction:
Popovers
This proposal allows opening and closing popovers with invoketarget
. This is already possible with popovertarget
, and so there is no new capability here.
Dialogs
This proposal allows opening and closing <dialog>
elements with invoketarget
. While this is new capability (prior to this proposal scripting is required), it poses the same security considerations as popovers in that it toggles the visiblity of a new element. Modal dialog elements are also placed on the “top layer” which means they occlude other content. This is already possible with popovers, which are also placed on the top layer, so while buttons to open <dialog>
elements is a new capability, it is not a novel new security threat considering attackers could already craft a popover.
Modal dialogs also trap focus, such that focus can only be moved within elements inside the dialog or the browser native controls (such as the URL bar). This is considered to not pose a significant threat as the invoking button requires user activation, the dialog cannot render outside of the document frame (iframe or browser window) and so cannot occlude the browsers Chrome, and there are built in gestures to close a dialog (such as the Esc key via a CloseWatcher
) that cannot be opted out of with or without invokers.
Details (proposed follow-on)
This proposal aims to allow opening and closing <details>
elements with invoketarget
. This is not considered to be new capability, as it is already possible to do this with the <summary>
element.
Input (proposed follow-on)
It should be noted that input pickers (like the file picker dialog) render native controls outside of the bounds of the document frame (iframe or browser window). Due to this, the security implications of invoking these elements should be carefully considered.
Scripts can call .showPicker()
on form elements, and the picker will open. There are some cross-origin restrictions, for example calling .showPicker()
on elements in a cross-origin context only works for <input type=file>
or <input type=color>
.
It is also possible to wrap an <input>
in a <label>
, where invoking the label will open the picker. This is quite common practice when trying to style <input type=file>
for example; dressing the <label>
to look like a <button>
and hiding the <input>
.
This proposal allows showing the pickers of input elements, for example an invoker can target an <input type=file>
and clicking the invoker will open the operating systems file picker dialog. As stated above, this is already possible with <label>
elements, but care should be taken to ensure that the same cross-origin restrictions apply for Invokers as they do for scripting.
Fullscreen elements (proposed follow-on)
Turning an element into a fullscreen needs to be carefully consdidered. Scripting already allows this via the .requestFullscreen()
API which, as the name suggests, may not always be successful given the UA context. This API requires the document context to be active, for example requiring a user activation (this is true of invokers in the general case), it also requires the Permissions Policy to allow for this behaviour, and requires the element to not already be on the Top Layer.
All of these constraints should also be true for invokers opening fullscreen elements. This means the only significant new behavioural difference between an invoker opening a fullscreen element vs the existing behaviour which requires scripting, is that sanboxed iframes which disallow scripting but allow fullscreen may now declare a button that can fullscreen. Consider the following:
<iframe sandbox allow="fullscreen">
<html id="invokee">
<button invoketarget="invokee" invokeaction="toggleFullscreen">Invoke</button>
<button onclick="invokee.requestFullscreen()">Go fullscreen</button>
</html>
</iframe>
In this example, the <button onclick>
will do nothing as the iframe is sandboxed and does not allow scripting. The <button invoketarget>
will fullscreen the element, however, as the allow=fullscreen
Permission Policy is enabled to allow fullscreen.
This is considered to be a low security threat, as it requires opt-in to the fullscreen Permission Policy explitly via iframe attributes or scripting. We will, however, continue to explore if this should be a possibility.
Media Elements (proposed follow-on)
This proposal allows video and audio elements to be controlled with buttons outside of the browsers native video controls. This includes playing, pausing, and toggling mute/unmute. Today this is only possible using scripting via the equivalent scripting APIs: .play()
/ .pause()
/ .muted=
. These APIs are guarded by Permissions Policy, and so should the invoker equivalent. Some User Agents can be configured to reject calls to .play()
, and this should also be true of invokers. As such the key new capability here is the ability to call these APIs without scripting enabled. This is effectively the same concern as fullscreen; the security model is guarded around the Permissions Policy.
There is also additional concern around the media element invokers being able to circumvent autoplay policies. Invokers should not be able to circumvent these. By design, invokers require a user interaction (clicking the button) and so this should not circumvent autoplay policies.
Further proposals…
There are continued suggestions for new capabilities of Invokers, such as invoking picture-in-picture.
Each of these will need to be taken into consideration on a case-per-case basis. The following questions will need to be answered for each of these:
- Is it possible for the user to do this today with scripting?
- Is it possible for the user to do this today without scripting?
- Does this behaviour bypass scripting sandboxing rules, such as Permissions Policy or iframes?
- Does this enable buttons to invoke content that appears outside of the browser frame?
PAQ (Potentially Asked Questions)
Why the name command
? Why not click
?
While click
is a fairly well established name in the world of the web, it is quite specific to the action from a user gesture, whereas invoke events notify a related element that an interactive control is requesting action. In other words, a click
happens on the element being interacted with - the invoker, while an invoke
happens on the element the interactive control points to - the invokee.
If the invokee were to recieve click events from the interactive control, this would make it difficult to differentiate user interaction on the element vs on invoking elements. Take for example a video player; clicking on the area which renders the video may play/pause the video, but clicking on a video control may cause different behaviours on the video. The extra controls invoke an action on the video but the user gesture was performed on the controls themselves.
Invokers rely on click events for their behavior to work, and they are not intended to replace them. Instead, invokers enhance click events to drive behaviour on a different element. Consequently they need a new conceptual name for this. Given the opportunity to supply a new name, invoke
was settled on.
Given the new conceptual model of “command” it stands to reason that all related concepts share the same name, as such commandfor
and command
were chosen rather than, say, clickfor
/clickaction
. Similarly the event class is named CommandEvent
and the event name command
.
Didn’t this used to be called invoke
? What happened?
The original proposal for Invoke Commands was to use the term invoke
exhaustively through the API, and so the attributes were originally named invoketarget
/invokeaction
and the event named InvokeEvent
with a type of invoke
. This was deemed too abstract, and standards groups and web developers desired a simpler name. For those interested in the history, some conversation that lead to changing invoke
to command
starts around here: https://github.com/whatwg/html/issues/9625#issuecomment-2115718679
Do we have to always supply both? Can’t we make command
or commandfor
implicit?
The original proposal had the concept of an “auto” command
value which would determine an exlicit command based on various heuristics, such as the target element. This has been deferred for the initial ship, but may be explored further. This is considred out of scope for the initial ship, however.
We may also explore the possibility of making commandfor
implicit, for example if a button is a descendant of a dialog, omitting commandfor
may make sense. This is also considered out of scope for the initial ship.
What about adding Invoker defaults for <form>
?
Defaults for <form>
are intentionally omitted as this proposal does not aim to replace Reset or Submit buttons. If you want to control forms, use those.
What about adding Invoker defaults for <a>
?
Defaults for <a>
are intentionally omitted as this proposal does not aim to replace anchors. If you intend to produce a page navigation, use an <a>
tag.
Why is command
/commandfor
limited to buttons?
This is by design, to allow for a “pit of success”; invoking actions on non-button elements such as <div>
s or <a>
s creates many problems, especially for non-interactive elements. While <a>
s are interactive, they should only be used for page navigation and not for invoking other behaviours, and so command
/commandfor
should not be allowed.
Why isn’t input[type=submit]
included?
This is not added by design. Submit inputs already have a default action: submitting forms. If you want a button to control the submission of a form, use input[type=submit]
, if you want a button to control invocation of something other than a form then you should use input[type=button]
.
Why is input[type=reset]
included?
It may stand to reason that if input[type=submit]
is excluded then so should input[type=reset]
, however, there are valid use cases to resetting a form at the same time as some other action, for example closing the dialog that contains a form:
<dialog id="my-dialog">
<form>
<input type="text" />
<!-- This button closes the dialog _and_ resets the form -->
<input type="reset" commandfor="my-dialog" command="close" value="Cancel" />
</form>
</dialog>
What does this mean for popovertarget
?
Whilst command
/commandfor
does replicate popovertarget
’s functionality, the two will co-exist side by side for some while to enable a smooth transition.
It is, however, intended that commandfor
will replace popovertarget
leading to popovertarget
’s eventual deprecation.
Why doesn’t command just call the JS API method?
HTML attribute values have different casing to method names, so for example an attribute value of show-modal
does not match the method name showModal()
. This requires manual mapping to the respective methods, but browsers - under the hood - effectively map from these values to the respective methods. Similarly, custom commands must be prefixed with a double dash (--
). It is possible to convert the dashed value to camel case, and use computed properties to call methods, if that’s desired:
<button commandfor="my-element" command="--spin-widget">
Spin the widget
</button>
<spin-widget id="my-element"></spin-widget>
<script>
customElements.define(
"spin-widget",
class extends HTMLElement {
connectedCallback() {
this.addEventListener("command", (e) => {
const method = e.command
.slice(2) // remove the double dash
.replace(/(-\w)/g, c => c[1].toUpperCase()); // dash-to-camel case
if (method in this) {
this[method](event);
}
});
}
spinWidget(event) {
// ...
}
},
);
</script>
Command seems limited, what if I wanted to add arguments?
Custom command
s can be used in a freeform way, for your own elements. If you feel it necessary you can invent your own DSLs for passing extra data using this hint. For example:
<button commandfor="my-counter" command="--add-1">Add 1</button>
<button commandfor="my-counter" command="--add-2">Add 2</button>
<button commandfor="my-counter" command="--add-10">Add 10</button>
<input readonly id="my-counter" value="0" />
<script>
const counter = document.getElementById("my-counter");
counter.addEventListener("command", (e) => {
let addMatch = /^--add-(\d+)$/.match(e.action);
if (addMatch) {
counter.value = Number(counter.value) + Number(addMatch[1]);
}
});
</script>
You can also leverage the buttons value
attribute to effectively parameterise certain commands:
<button commandfor="my-counter" command="--add-num" value="1">Add 1</button>
<button commandfor="my-counter" command="--add-num" value="2">Add 2</button>
<button commandfor="my-counter" command="--add-num" value="10">Add 10</button>
<input readonly id="my-counter" value="0" />
<script>
const counter = document.getElementById("my-counter");
counter.addEventListener("command", (e) => {
if (e.command == '--add-num') {
counter.value = Number(counter.value) + Number(e.invoker.value);
}
});
</script>