html
html copied to clipboard
Proposal: Customized built-in elements via `elementInternals.type`
What problem are you trying to solve?
Web component authors often want to create custom elements that inherit the behaviors and properties of native HTML elements. These types of custom elements are referred to as "customized built-in elements" or just "customized built-ins". By customizing built-in elements, custom elements can leverage the built-in functionality of standard elements while extending their capabilities to meet specific needs. Some of the use cases enabled by customized built-ins are listed below.
- Custom buttons can provide unique styles and additional functionality, such as split or toggle button semantics, while still maintaining native button behavior such as being a popover invoker.
- Custom buttons can extend native submit button behavior so that the custom button can implicitly submit forms. Similarly, custom buttons that extend native reset button behavior can implicitly reset forms.
- Custom labels can provide additional functionality, such as tooltips and icons, while still supporting associations with labelable elements via the
forattribute or nesting a labelable element inside the custom label.
What solutions exist today?
A partial solution for this problem already exists today. Authors can specify the extends option when defining a custom element. Authors can then use the is attribute to give a built-in element a custom name, thereby turning it into a customized built-in element.
Both extends and is are supported in Firefox and Chromium-based browsers . However, this solution has limitations, such as not being able to attach shadow trees to (most) customized built-in elements. Citing these limitations, Safari doesn't plan to support customized built-ins in this way and have shared their objections here: https://github.com/WebKit/standards-positions/issues/97#issuecomment-1328880274. As such, extends and is are not on a path to full interoperability today.
How would you solve it?
The ElementInternals interface gives web developers a way to participate in HTML forms and integrate with the accessibility OM. This will be extended to support the creation of customized built-ins by adding a type property, which can be set to string values that represent native element types. The initial set of type values being proposed are listed below. Support for additional values may be added in the future.
'' (empty string)- this is the default value, indicating the custom element is not a customized built-inbutton- for button like behaviorsubmit- for submit button like behaviorreset- for reset button like behaviorlabel- for label like behavior
This proposal was presented and discussed at TPAC, where it was resolved by the WHATWG to add elementInternals.type = 'button' to the HTML spec.
Initial proposal discussed at TPAC: https://github.com/openui/open-ui/issues/1088#issuecomment-2366092981
WHATWG resolution for elementInternals.type = 'button': https://github.com/openui/open-ui/issues/1088#issuecomment-2372520455
This issue formally tracks the proposal against the @whatwg/html repo and also includes a few additional types beyond button that had support at TPAC.
Full explainer here: https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/ElementInternalsType/explainer.md
Examples
Below is an example showcasing a custom button being used as a popup invoker. When the custom button is activated, ex. via a click, div id="my-popover will be shown as a popover.
class CustomButton extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals_ = this.attachInternals();
this.internals_.type = 'button';
}
}
customElements.define('custom-button', CustomButton);
<custom-button popovertarget="my-popover">Open popover</custom-button>
<div id="my-popover" popover>This is popover content.</div>
Like with native buttons, if the disabled attribute is set, a custom button cannot be activated and thus cannot invoke popovers.
Below is an example showcasing a custom submit button being used to submit a form. When the custom button is activated, ex. via a click, the form will be submitted and the page will navigate.
class CustomSubmitButton extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals_ = this.attachInternals();
this.internals_.type = 'submit';
}
}
customElements.define('custom-submit-button', CustomSubmitButton);
<form action="http://www.bing.com">
<custom-submit-button>Submit</custom-submit-button>
</form>
If the disabled attribute is set on a custom submit button, it cannot be activated and thus cannot submit forms.
Below is an example showcasing a custom label being used to label a checkbox. When the custom label is activated, ex. via a click, the checkbox is also activated, resulting in its state changing to checked.
class CustomLabel extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.internals_ = this.attachInternals();
this.internals_.type = 'label';
}
}
customElements.define('custom-label', CustomLabel);
<custom-label for='my-checkbox'>Toggle checkbox</custom-label>
<input type='checkbox' id='my-checkbox' />
cc: @alexkeng
Not sure about the "type" property proposed here but enhancing ElementInternals to be able to implement more builtin-element-like behaviors seems like a good direction. Being able to implement a custom element which acts like a submit button, for example, seems like a natural enhancement to ElementInternals. Being able to implement a custom element which acts like a label also seems like a useful extension.
I think I'd prefer breaking apart features of these builtin elements and adding each capability to ElementInternals. That'll allow a custom element to combine a set of features even if a particular combination didn't exist as a builtin element.
I think I'd prefer breaking apart features of these builtin elements and adding each capability to
ElementInternals. That'll allow a custom element to combine a set of features even if a particular combination didn't exist as a builtin element.
If we're after more compositional "mixin" style features I wonder if we might consult with Lit folks (/cc @justinfagnani, @sorvell to name a few) about what an API shape might look like, especially with their work around the "Reactive Controllers" pattern. At the risk of scope creeping, providing a mixin pattern that allows for applying built-in behaviours, but also mixing in userland behaviours could be a really interesting concept.
import {CustomMixin} from './my-mixin.js'
class CustomButton extends HTMLElement {
constructor() {
super()
const internals = this.attachInternals();
// Add the built-in mixin for button activation behaviour, gives us `popovertarget`, `commandfor`, etc.
internals.addMixin(HTMLButtonActivationBehaviorMixin);
// Also adopt my custom mixin, which can do other things...?
internals.addMixin(CustomMixin);
}
}
Not sure about the "type" property proposed here but enhancing
ElementInternalsto be able to implement more builtin-element-like behaviors seems like a good direction. Being able to implement a custom element which acts like a submit button, for example, seems like a natural enhancement toElementInternals. Being able to implement a custom element which acts like a label also seems like a useful extension.
Glad to hear!
I think I'd prefer breaking apart features of these builtin elements and adding each capability to
ElementInternals. That'll allow a custom element to combine a set of features even if a particular combination didn't exist as a builtin element.
If we break up the features and support them individually on ElementInternals (elementInternals.disabled, elementInternals.popoverTarget, elementInternals.for, elementInternals.control, etc.), developers will not have a way to get all the behaviors of a built-in at once. They will have to "pick and choose" the right ones and it could cause a bad user experience if they get that wrong. For example, if the author is creating a customized built-in button, it wouldn't get the button ARIA role implicitly and the author would need to remember to set elementInternals.role explicitly. With elementInternals.type, the custom button would get the implicit role in-built. An example of another quirk would be if a developer built a button that supports elementInternals.popoverTarget but didn't support elementInternals.disabled. Such a button could invoke a popover, like built-in buttons, but couldn't be disabled by the disabled attribute, unlike built-in buttons. That seems inconsistent, and unexpected given the goal is to provide built-in-like behavior.
Using elementInternals.type ensures that all these come in a "package", so the customized built-in always gets all the behaviors it needs to function like the corresponding native element.
I think the proposal has merit, however I noticed that the API is somewhat unusual:
elementInternals.typeshould only be set once. IfelementInternals.typehas a non-empty string value and is attempted to be set again, a "NotSupportedError" DOMException should be thrown
To me, the requirement that the type only be assigned once tells me that it should not be a mutable property at all, but a parameter when constructing the ElementInternals instance. So instead of introducing a type property, we should be introducing an optional options parameter to attachInternals with a type property.
The value of the type should continue to be accessed as a property, but now it can be a read-only property with the same common behaviors as other read-only properties, rather than introducing surprising and unexpected behaviors.
If we break up the features and support them individually on
ElementInternals...developers will not have a way to get all the behaviors of a built-in at once.
Bitwise flags come to mind, where ElementInternals.BUTTON_TYPE could be the same as ElementInternals.SUBMIT_BEHAVIOR | ElementInternals.KEYBOARD_CLICK_BEHAVIOR | ElementInternals.SOME_OTHER_BEHAVIOR | .... Unfortunately that would be very difficult to set up in a way that supports backwards compatibility when we ultimately decide that there are other newer behaviors we'd like to support. Alternatively we could accept a string of space-separated tokens, or array of strings, or an object with properties.
I think the thing we'll want to identify is whether there are legitimate use-cases to opt in or out of some of the built-in behaviors. If there are, the API should enable those use cases.
That said, the API should make it easy to "fall into the pit of success" so that the common use cases (e.g. type = 'button') are trivially easy.
To me, the requirement that the
typeonly be assigned once tells me that it should not be a mutable property at all, but a parameter when constructing theElementInternalsinstance. So instead of introducing atypeproperty, we should be introducing an optionaloptionsparameter toattachInternalswith atypeproperty.
@zzzzBov this is great feedback! we agree that a mutable property is unnecessary, and an optional parameter to attacheInternals is a better direction. We'll update the explainer accordingly.
I think the proposal has merit, however I noticed that the API is somewhat unusual:
elementInternals.typeshould only be set once. IfelementInternals.typehas a non-empty string value and is attempted to be set again, a "NotSupportedError" DOMException should be thrownTo me, the requirement that the
typeonly be assigned once tells me that it should not be a mutable property at all, but a parameter when constructing theElementInternalsinstance. So instead of introducing atypeproperty, we should be introducing an optionaloptionsparameter toattachInternalswith atypeproperty.The value of the
typeshould continue to be accessed as a property, but now it can be a read-only property with the same common behaviors as other read-only properties, rather than introducing surprising and unexpected behaviors.
I like this idea, but I'm wondering if that makes it hard to customize the type based on the custom element's attributes? For example:
<my-button type="submit">Submit</my-button>
<my-button type="reset">Reset</my-button>
My understanding is that you should not / cannot access attributes in the constructor, so the attachInternals({type}) would require you to create separate custom elements for each button type?
Separately, could this proposal also support a custom element that acts as a Form element? e.g. type: 'form'
https://github.com/whatwg/html/issues/10220
I'm wondering if that makes it hard to customize the
typebased on the custom element's attributes?
Either version of the proposal would make that approach impossible/inadvisable due to the fact that the type was meant be assigned exactly once, and attributes are mutable.
I don't have a strong argument against leaving it as a type property and allowing the value to be mutable.
I will say that <input> and <button>s use of type attributes to drastically change behavior for those elements is one of the most flagrant violations of the single responsibility principle, and your custom elements are probably better off by making use of different elements for different behaviors. In my own experience I can't think of a single time that I've ever changed a type attribute after an interactive element was initialized.
Related: it should be possible to make an element behave like a form https://github.com/whatwg/html/issues/10220
This may require additional APIs, eg a way to submit the form, so it'd be good to come up with a pattern that doesn't design the platform into a corner.
This is a great alternative to built-in extends (is="" attribute), but should be noted this doesn't cover the progressive enhancement use case. For example, if JavaScript is turned off (or takes a long time to load), then this,
<form>
<cool-button></cool-button>
</form>
won't work, as the element will not do anything because JavaScript is required for ElementInternals. Same with <table>, etc.
Progressive enhancement is probably the most important thing that people need is="" for, because it cannot be solved with JavaScript (it needs to provide a suitable fallback in the absence of JavaScript). The rest, making custom buttons, etc, can already be done with JavaScript, despite if it is only more cumbersome but at least not impossible. Due to odd parsing rules, certain things like <table><foo-bar></foo-bar></table> simply won't work with or without JavaScript, due to how the parser sets up the DOM during parsing.
In this sense, the idea in this thread would be useful for making some things easier to do, but is not a complete alternative to customized built-in elements. This thread's idea would be a way to allow elements to gain the same behaviors as built-in elements using JavaScript, rather than extending them.
See the following issues for alternatives that would also cover progressive enhancement and odd parsing rules (all of which also do not extend built-ins but provide addon behaviors):
- https://github.com/WICG/webcomponents/issues/727
- https://github.com/WICG/webcomponents/issues/1029
Should those topics be moved here to whatwg?
I'm not sure that using ElementInternals for this makes sense. So far that API doesn't really change basic element behavior. For example, formAssociated is not set via ElementInternals, but when an element is configured with it, functionality is exposed via ElementInternals.
The explainer notes that type can only be set once, but this means it can change (once) at any time in the element's lifecycle. This is likely unneeded and probably undesirable. Perhaps instead, "type" behavior should be configured similar to formAssociated via a static property:
class BehavesLikeButton extends HTMLElement {
static formAssociated = true;
static behavesLike = 'button';
constructor() {
super();
this._internals = this.attachInternals();
console.log(this.disabled); // logs `false`
}
}
Perhaps instead, "type" behavior should be configured similar to formAssociated via a static property:
agreed this approach could provide a cleaner design and it also aligns with the proposal mentioned here for making an element behave like a form. (also agreed with @jakearchibald that form is a good candidate, but we'll focus on button and label to start)
I've filed https://github.com/whatwg/html/issues/11390 for further discussion.
This is interesting what it enables, but odd in how it does: certain features will be runtime mixins of a form that cannot be implemented in userland, basically.
Can we use actual mixins instead?
class MyEl extends FormButton(HTMLElement) {}
Mixins enable type safety out of the box in TypeScript.
I think this proposal is a bandaid solution that is overfit to the most prominent pain points, but in a way that does not architecturally fit the rest of the web platform. While it is tempting to just ship something, anything ASAP to fix the most pressing issues, any architectural weirdness we introduce is weirdness we will likely need to maintain forever, or at least for decades to come.
If this came to the TAG for a design review during my tenure, I'd raise these issues:
- It involves an alternative "magic" inheritance mechanism that doesn't go through
extends, which is the JS-native way to do class inheritance.superstill resolves toHTMLElementnot e.g.HTMLLabelElementorHTMLButtonElement, and any inherited properties/methods can not be customized via the usual JS mechanisms. - The values values are an odd mix of element types (
button,label) and values (submit,reset). - The resulting element inherits some properties from the parent but not others, and there is no way for authors to conceptualize and predict which ones or pull in fewer or more.
- The design violates several TAG principles around how properties are supposed to work, such as having IDL attributes with (huge) side effects or throwing on setters.
I just filed https://github.com/WICG/webcomponents/issues/1108 with an alternative that attempts to reuse existing web platform concepts such as class inheritance and slots. It's definitely more ambitious though: bigger scope, but also more powerful + much broader variety of use cases.
I think this proposal is a bandaid solution that is overfit to the most prominent pain points, but in a way that does not architecturally fit the rest of the web platform. While it is tempting to just ship something, anything ASAP to fix the most pressing issues, any architectural weirdness we introduce is weirdness we will likely need to maintain forever, or at least for decades to come. If this came to the TAG for a design review during my tenure, I'd raise these issues:
@LeaVerou The solution is intentionally not using extends because inheritance is not always appropriate; a customized button like <fancy-button> doesn’t necessarily want everything from the native <button>. However, based on years of developer feedback, we know that <button> does have essential behaviors that authors repeatedly reimplement or ask browser vendors to expose when there is no viable workaround, so we introduced this opt-in mechanism that enables certain native capabilities, giving web component authors a better starting point so they don’t have to build everything from scratch. (also WebKit has long been opposed to the established inheritance-based approach to solving this class of problems (https://github.com/WebKit/standards-positions/issues/97), so using inheritance as the mechanism is unlikely to be interoperable)
button and label are just the starting point that's chosen based on what we understand to be the most pressing developer needs. The solution can be extended to other elements where there's user demand, so we don’t see it as overfitting
Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in https://github.com/whatwg/html/issues/11390 addresses both these concerns. The idea is that instead of using a mutable ElementInternals.type, we'd use a static behavesLike property that would support only (button, label).
Thank you @LeaVerou for commenting on this, you managed to articulate what I failed to at (several) TPACs and in https://github.com/whatwg/html/issues/11061#issuecomment-2673966134. I'm hesitant about the proposed solution because it feels a little too narrow in scope and I worry we'll be limiting ourselves in future, and I also would love to find a solution which allows web component authors to "bring their own" behaviours (or polyfill emerging ones).
My concerns with both attachInternals().type = 'button' or static behavesAs = 'button':
- Developers consistently provide feedback that string APIs are not desired. They can be brittle (for example what does
static behavesAs = 'buton'do? Likely it can't throw). - They're not super discoverable, one cannot reason about them or the behaviours they will do in code alone, i.e. is no additional API surface to discover, and their behaviour cannot be reasonably explained with JavaScript. They're not explained by other primitives in the platform.
- There is no defined path or carve-out for userland extension. For example if I want
static behavesAs = 'frobulate'I can't, so I'm left to invent new ways or hack my way around to achieve the same effect. - There's no scope for composition. While
labelandbuttonare likely mutually exclusive behaviours, we're locking ourselves into mutually exclusive behaviours. If we later decide to add a behaviour likeimage, we're limiting users from the ability to have bothimage+buttonbehaviours.
It seems really unfortunate to me that we're exploring solutions for this shape of problem which, to me, is a missing feature of JS itself. If we were in C++ land we'd simply reach for multiple inheritance, or if in Rust we'd use a (dyn) trait, Swift we'd use a protocol, Kotlin we'd use an interface, in Ruby an include, the list goes on.
@LeaVerou The solution is intentionally not using
extendsbecause inheritance is not always appropriate; a customized button like<fancy-button>doesn’t necessarily want everything from the native<button>.
Inheritance doesn't mean getting everything from the parent — that's what overrides are for.
However, based on years of developer feedback, we know that
<button>does have essential behaviors that authors repeatedly reimplement or ask browser vendors to expose when there is no viable workaround, so we introduced this opt-in mechanism that enables certain native capabilities, giving web component authors a better starting point so they don’t have to build everything from scratch.
Oh I 100% agree that this is a problem that needs solving, I just have concerns about this proposed solution.
On that note, it would be useful for this discussion to make a more detailed/exhaustive list of these behaviors that authors repeatedly reimplement. It may reveal alternative solutions that we have not yet considered.
(also WebKit has long been opposed to the established inheritance-based approach to solving this class of problems (WebKit/standards-positions#97), so using inheritance as the mechanism is unlikely to be interoperable)
I’m well aware. But WebKit being opposed to that particular inheritance-based proposal (for good reasons) does not necessarily translate to them being opposed to any inheritance-based design.
buttonandlabelare just the starting point that's chosen based on what we understand to be the most pressing developer needs. The solution can be extended to other elements where there's user demand, so we don’t see it as overfittingRegarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in #11390 addresses both these concerns. The idea is that instead of using a mutable
ElementInternals.type, we'd use a staticbehavesLikeproperty that would support only (button,label).
I may have missed it but I don't see how that addresses any of the concerns I listed except the one around mixing attributes and element names in the same value space?
Additionally, because it is not inheritance based, it introduces a new problem: now components cannot have their own type attribute with different values and semantics if they want their component to behave like a button.
Edit: I wrote this before seeing @keithamus's comment above. +100 to literally everything in it.
It seems really unfortunate to me that we're exploring solutions for this shape of problem which, to me, is a missing feature of JS itself. If we were in C++ land we'd simply reach for multiple inheritance, or if in Rust we'd use a (dyn) trait, Swift we'd use a protocol, Kotlin we'd use an interface, in Ruby an include, the list goes on.
JavaScript does have mixins, in the form of subclass factories: https://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/
On that note, it would be useful for this discussion to make a more detailed/exhaustive list of these behaviors that authors repeatedly reimplement. It may reveal alternative solutions that we have not yet considered.
I think this is a very important thing to do. This proposal does enumerate a list of attributes for each type, but it does not mention events, properties, or methods. Properties and methods are extremely important to think of how to handle, because in this proposal there is no reason to suspect that a static property on a class would suddenly induce new properties on it. That's certainly possible in a language as dynamic as JavaScript, but extremely rare for good reason. It would be very difficult to statically analyze, and exacerbate the fragile base class problem.
(also WebKit has long been opposed to the established inheritance-based approach to solving this class of problems (WebKit/standards-positions#97), so using inheritance as the mechanism is unlikely to be interoperable)
I’m well aware. But WebKit being opposed to that particular inheritance-based proposal (for good reasons) does not necessarily translate to them being opposed to any inheritance-based design.
My recollection of the WebKit position was that there were too many underspecified behaviors spread across the native elements to safely allow wholesale subclassing of them, and that they might be open to the idea of subclassing if someone could do the work and specify what should happen in all the cases (adding ShadowRoots, overriding connectedCallback, etc.)
This proposal starts with a narrow set of behaviors and seems to strive to specify them all, which is great and might just address WebKit's main complain with subclassing altogether. ie, instead of subclassing HTMLButtonElement a class would subclass something with the specific behaviors specified by the 'button' type in this proposal.
One thing that's important to not here is that this proposal really is a form of subclassing, just hidden from JavaScript. It's making HTMLElement itself polymorphic and able to express many different classes hidden within it.
I think this might become more clear if the proposal talked about attribute reflection and properties. Does type = 'button' mean that the attributes added also have the respective properties that they reflect to on HTMLButtonElement? Does the element get popoverTargetElement, etc.? How is this expressed in terms of the prototype chain? Are these all optional properties on HTMLElement?
IMO, this would be much more explainable if the mechanism for mixing in behavior was something that could be expressed in JavaScript, like a mixin. That would add new prototypes to the prototype chain, which have the new native-backed properties.
a customized button like
doesn’t necessarily want everything from the native Inheritance doesn't mean getting everything from the parent — that's what overrides are for.
By everything I mean including things that overrides can't handle, eg the limitation of tweaking the shadow dom
Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in #11390 addresses both these concerns.
I may have missed it but I don't see how that addresses any of the concerns I listed except the one around mixing attributes and element names in the same value space?
My understanding is that the principles you referred to (6.2 and 6.4) apply to IDL attributes, and therefore do not appear to apply to the static property approach. In fact, 6.2 and 6.4 focus on the getters of IDL attributes, so even with the .type approach, I wouldn't see it as violating those principles, since the .type getter doesn’t throw or have side effects.
IMO, this would be much more explainable if the mechanism for mixing in behavior was something that could be expressed in JavaScript, like a mixin. That would add new prototypes to the prototype chain, which have the new native-backed properties.
Regarding the mixin approach, I agree it'll be more discoverable if we can see the added mixin in the prototype chain, however, there are some built-in behaviors that can't be captured by mixin, eg, the strict element hierarchy mentioned in this comment, or implicit label association, or the implicit submission mentioned in this comment. It appears if we were to pursue the mixin approach, we'd need a way to signal these implicit behaviors, eg adding a flag in the mixin, which essentially bring us back to our static property approach. Also, our current proposal supports only the relevant attributes and behaviors (eg, implicit label association, form submission etc); attribute reflection/properties/events/methods are out of scope and would still need to be implemented by authors, so there's really nothing new to expose in the prototype chain.
They're not super discoverable, one cannot reason about them or the behaviours they will do in code alone
If the mental model is, if behavesLike=button, all button attributes will be supported, would that be helpful? Or we can even explicitly enumerate what attributes we want (if not specified, default is all attributes):
static behaveLike = "button";
static supportedAttributes = ["popovertarget", "formnovalidate", "type"];
There is no defined path or carve-out for userland extension. For example if I want static behavesAs = 'frobulate' I can't, so I'm left to invent new ways or hack my way around to achieve the same effect
I am not sure I follow. If the goal is to reuse behaviors from a non-native element, couldn’t we just extend it? or do you mean you want to define a new collection of behaviors from native elements?
There's no scope for composition. While label and button are likely mutually exclusive behaviours, we're locking ourselves into mutually exclusive behaviours. If we later decide to add a behaviour like image, we're limiting users from the ability to have both image+button behaviours.
I am not sure if it's practical to create an element that combines capabilities from multiple built-in elements. These built-ins exist for a reason, and trying to blur their boundaries is likely to introduce unnecessary corner cases that could become unmanageable. I think we'd need to see more real-world use cases where we can't use <button><image></button> and <image+button> is the only viable option before we consider this direction.
@alexkeng
a customized button like doesn’t necessarily want everything from the native .
Inheritance doesn't mean getting everything from the parent — that's what overrides are for.
By everything I mean including things that overrides can't handle, eg the limitation of tweaking the shadow dom
The inheritance-based proposal I linked to does cover that, through a mechanism of wrapping shadow roots (or discarding them entirely when they're not useful).
Regarding the concerns about mixed semantics and TAG principle violations, we believe that the modifications we're proposing in #11390 addresses both these concerns.
I may have missed it but I don't see how that addresses any of the concerns I listed except the one around mixing attributes and element names in the same value space?
My understanding is that the principles you referred to (6.2 and 6.4) apply to IDL attributes, and therefore do not appear to apply to the static property approach. In fact, 6.2 and 6.4 focus on the getters of IDL attributes, so even with the
.typeapproach, I wouldn't see it as violating those principles, since the.typegetter doesn’t throw or have side effects.
Throwing is not the only possible side effect. The element class sprouting a bunch of IDL attributes that are defined either directly in the class or inherited is also a side effect.
@alexkeng
our current proposal supports only the relevant attributes and behaviors (eg, implicit label association, form submission etc); attribute reflection/properties/events/methods are out of scope and would still need to be implemented by authors, so there's really nothing new to expose in the prototype chain.
I think that's a pretty critical hole in the current proposal then.
I think it would be pretty unexpected for a native-button-like element to have button-like attributes and not the button-like properties. It's especially bad to support the commandfor and popovertarget attributes, but not have commandForElement and popoverTargetElement properties, since those are a lot more than just returning the value of an attribute.
I'm also confused by the inclusion of labels for the button type, since this isn't an attribute, but a property (it's an IDL attribute) that reflects the NodeList of associated labels. What would it mean to support labels if properties can't be added to the prototype?
Were I in position to, I'd probably oppose this proposal based on this. At the very least there should be some way to get and set all attribute-associated properties, if not via get/setAttribute, then via ElementInternals like labels and form currently are. Though to me this is really a reason to go with a mixin-based approach in the first place.
@justinfagnani I agree that supporting button-like properties would make the API surface more complete. One possible solution could be to introduce dedicated interfaces, eg. ButtonInternals / LabelInternals (names TBD) within ElementInternals:
interface ButtonInternals {
attribute Element? popoverTargetElement;
attribute DOMString popoverTargetAction;
attribute Element? commandForElement;
};
interface LabelInternals {
attribute DOMString htmlFor;
attribute Element? control;
};
interface ElementInternals {
attribute ButtonInternals? buttonMixin;
attribute LabelInternals? labelMixin;
}
Example usage:
class CustomButton extends HTMLElement {
static behavesLike = "button";
constructor() {
super();
this.internals = this.attachInternals();
}
get popoverTargetElement() {
return this.internals.buttonMixin?.popoverTargetElement ?? null;
}
set popoverTargetElement(element) {
if (this.internals.buttonMixin) {
this.internals.buttonMixin.popoverTargetElement = element;
}
}
}
This approach not only provides authors with a clear and discoverable API surface (rather than exposing these directly on ElementInternals), but also avoids potential property name collisions. It follows an existing pattern where some ElementInternals properties are enabled based on a static property, eg. .form/.labels are enabled by formAssociated = true, vs. .buttonMixin enabled by behavesLike = "button".
Our current proposal supports only attributes and implicit behaviors because we believe these alone could already provide meaningful value to authors and property support could be added incrementally later, but we are open to including properties in the first release if that makes more sense to the community. The main tradeoff is additional design/implementation time, eg. should the properties be added at the element level: fancy_button.popoverTargetElement? or the ElementInternals level: fancy_button.internals.popoverTargetElement? or the dedicated internals level: fancy_button.internals.buttonMixin.popoverTargetElement? (personally I think the last option makes the most sense.)
Finally, regarding the inclusion of the labels property in Button (or the control property in Label) in the explainer, that’s actually outdated info. Apologies for the confusion; we’ll update the explainer with the latest details soon.
We discussed this topic a bit in this morning's WHATNOT. I was asked to give a bit of a historical perspective.
I think the overall goal of enabling custom elements to do everything built-in elements can do is a great one.
I think it is bad to do this via something like "make this element behave like tagName". That is too vague. Do you get the accessibility semantics? (Probably it should not, as now we have three layers, ARIA, ElementInternals, and your new thing.) Do you get the default CSS styles? Do you get the default activation behavior? Do you get their focus behavior? Do you get added to the appropriate element categories? (Labelable, submittable, etc. etc.) Nobody knows the answers with an API like that.
The API described here seems to on the surface resemble that antipattern. But I get the impression its authors are thinking deeply about these issues and their actual proposal is more subtle. I suggest they work hard to clarify exactly what algorithms/definitions in the spec they intend to modify, so that it can become clearer.
Historically, how we have done this is to decompose elements into various specific pieces of functionality, and expose them via ElementInternals. Examples include form-associated custom elements (FACEs) and accessibility semantics (ARIAMixin). (Arguably shadow DOM itself is another such example.)
This pattern means developers then have to expose the appropriate native element-like APIs via boilerplate wrapping. For example, lots of stuff like get form() { return this.#internals.form; }, or checkValidity() { return this.#internals.checkValidity(); }.
I have heard developers mention that this is unfortunate, and they'd prefer something more automatic. That is where a lot of the comments about mixins and inheritance seem to come in. I hear you. However, I think we should separate out the pattern we have, which works, for full-fidelity functionality, from any sugar layers that would make it easier to create this boilerplate. Personally, I think those sugar layers are a great fit for userland libraries, but if people want, we could explore something like CustomElementUtils.addFormAssociatedWrappers(MyElementConstructor), or something.
If we can hold our nose about the boilerplate, I think our current pattern is good. I think it gives you all the tools you need to create powerful new elements, with all the capabilities of native elements. I think we should keep doing it, with other things like https://github.com/whatwg/html/pull/5120.
Things get a little tricky when the behaviors we're exposing are not as general as https://github.com/whatwg/html/pull/5120, but instead are currently specific in the spec to 1-2 elements. That is where people can become tempted toward the "make this element behave like tagName" pattern. And then developers ask, can't you also give me all the boilerplate so I have the same API as tagName?
But if we recontextualize in the context of the historical project here, I think it's still very reasonable to say "submit buttons get special treatment in a few places in the spec. Custom elements should also be able to get that treatment." And then execute on our usual playbook, of adding something to ElementInternals.
Now, some specific API design considerations:
-
Historically, we have tried to make custom elements "inheritable", by putting some opt-ins as static properties (i.e.
formAssociated = true) instead of onElementInternalsdirectly (i.e., a hypotheticalthis.#internals.formAssociated = true). That is, if you haveMyFormAssociatedBaseClass, and then registerclass MyElement : MyFormAssociatedBaseClass {}, it should still end up form associated.I don't know how important this actually is. In particular, inheritance is less popular than we anticipated it being in 2016. But, absent a reason otherwise, I would probably continue defaulting to "static opt-in, which cases new parts of
ElementInternalsto start working", just like FACEs. -
Whenever possible, we should allow new elements to remix and include all their favorite parts of existing elements. This is another thing that is not possible with "make this element behave like
tagName".In this particular case, maybe it is the case that being a button, submit button, reset button, and label should all be mutually exclusive. But that argues for a less generic name than
type(orbehavesLike). Because what if I want something that behaves like both a submit button, and an image? Both a label, and anhr?I can't tell a better name before we perform the step mentioned above of analyzing exactly which parts of the spec you want to modify; are you mostly concerned about their effects on forms? On popovers? On focus? Etc.
-
Deciding how big or how small of a chunk to expose is a balancing act. Right now various things are tied up in being a button: e.g., interaction with forms, and interaction with popovers. Should we actually expose two APIs, so that an element can opt in to interacting with popovers without interacting with forms? Or only interacts with forms, and not popovers? Maybe not; maybe it's simpler for implementations to just keep those bundled together, and there's no known use case for separating them. But be careful.
We discussed this topic a bit in this morning's WHATNOT. I was asked to give a bit of a historical perspective.
I think the overall goal of enabling custom elements to do everything built-in elements can do is a great one.
I think it is bad to do this via something like "make this element behave like
tagName". That is too vague. Do you get the accessibility semantics? (Probably it should not, as now we have three layers, ARIA, ElementInternals, and your new thing.) Do you get the default CSS styles? Do you get the default activation behavior? Do you get their focus behavior? Do you get added to the appropriate element categories? (Labelable, submittable, etc. etc.) Nobody knows the answers with an API like that.The API described here seems to on the surface resemble that antipattern. But I get the impression its authors are thinking deeply about these issues and their actual proposal is more subtle. I suggest they work hard to clarify exactly what algorithms/definitions in the spec they intend to modify, so that it can become clearer.
Thanks @domenic, I can definitely appreciate this. I think if I were to boil down the underlying intent, it's that while custom elements are the defacto "component model for the web", they aren't necessarily getting first-party support when we build out new features. Popover as an example was intended to be more generic and was constrained to native elements with semantics to ensure accessibility. That's fine and righteous but there's not a way to get support there without recreating the entire API in JS. We have FACE, but we remain plagued by 814. The ask for an anchor attribute will hopefully solve issues around anchored positioning support for custom elements. :has support for shadow dom seems like it may finally be coming, but support within :host was originally missing.
There's also the reality that when creating custom elements there is some special magic that the platform obfuscates for native elements that we just can't. Radios get associated via HTML automatically whereas custom element radios are "ARIA" radios and must be associated with a wrapping element which limits our ability in markup (no tables ¯_(ツ)_/¯ ). Similarly, a combobox or custom element select requires some kind of trigger which has to exist in the DOM, but this is obfuscated with native selects causing assistive tech behavior to feel different/odd. I'd love to use customizable select, but we're back in spec land because we can't slot into the native select element. These are all understandable but nevertheless unfortunate deltas where custom element authors are constrained.
I think the tension I feel is that custom elements are constantly championed as a use case for these new API's and then when we get to implementation support is suddenly lacking or missing completely.
What we're ultimately after can be summarized with this from this sentance:
Whenever possible, we should allow new elements to remix and include all their favorite parts of existing elements.
I ultimately don't have a preference for how we get there or the shape of the API, I'd just like to use these with custom elements because most all of them provide real, tangible value, that otherwise can only be recreated with loads of JavaScipt (if at all).
This was briefly discussed in https://github.com/whatwg/html/issues/11622 today, see minutes. But we punted it to the next meeting when @dandclark can attend.
I think @domenic's preferred approach of adding smaller new features to ElementInternals makes a lot of sense, because it's an obvious path to get started and allows for boilerplate reduction in the future.
Importantly, it takes the pressure off figuring out the shape of the boilerplate reducer just to satisfy the more pressing need of allowing custom elements to have submit, label, popover, or commandfor functionality. As long as a userland utility like a mixin can be written.
I personally think that mixins are the right API shape for bundling behaviors together, but the DOM hasn't vended a mixin yet that I'm aware of, so there might be a high bar to clear for adding the first.
Adding individual features to ElementInternals means we can propose this more general behavior bundling mechanism on its own, and the layering means that the platform has a clear path to vend these bundles in the future.
Inheritability
Inheritability is a double-edged sword.
I do think it's important to preserve inheritability at least somewhat (like FACE) in the API. Inheritance is used commonly with custom elements because they're already so often written as classes. Things like FormAssociated() mixins are a common way to wrap up the class static, ElementInternals, and public API parts that developers want out of boilerplate-reducers:
class MyInput extends FormAssociated(HTMLElement) { ... }
Mixins have a lot of flexibility to interact with imperative APIs like ElementInternals because they can override the constructor and such, but I see that mixins sometimes have to restore inheritability where it doesn't exist in the base API, but in a bespoke way. For instance ElementInternals#role:
class MyInput extends FormAssociated(HTMLElement) {
static role = 'textbox';
}
The mixin then sets the role in the constructor.
Besides inheritability, most devs find the static property to be much simpler to use.
On the other hand, static properties limit the value to being the same across all instances of a class. We've already had cases where we want elements to be FACE or not on a per instance basis. Admittedly, this was for polyfilling scoped custom element registries where two classes registered to the same tag name might have different statics, but as an observation only the static properties like formAssociated or observedAttributes were a problem.
I'm unsure if there are non-polyfill use cases for setting these options on a per-instance basis.
It is interesting that a mixin can opt-in to exposing an ElementInternals API as a static class API. Maybe something like that is a way to have both.
@alexkeng I'd love for you all to explore the direction @domenic outlines above. Small building blocks (e.g., maybe we can finally solve focus for custom elements) seems like the most likely way to success here.
Discussed in https://github.com/whatwg/html/issues/11648#issuecomment-3293932780.
Thank you all for the great discussion and feedback!
We have an updated explainer that includes these 4 major changes based on feedback here and WHATNOT discussions in last few weeks:
- Instead of exposing all button behaviors using
static behavesLike = "button", we are focusing on exposing the activation behaviors only:static ButtonActivationBehaviors = true. - We’ll expose only
command/commandForinitially, with submit/reset as fast follows. - ARIA behaviors (focusability, role=button) will be automatically included based on the ARIA WG’s overwhelming consensus.
- form association will NOT be automatically included. (TBD if we want to do it with submit/reset)
We believe this strikes a good balance, but we’d appreciate any additional thoughts.