Skip to content

Commit 788e1ce

Browse files
authored
Addressing feedback I (#1232)
Updates: - Deleted paragraph on "behavior" vs "mixins". Will wait for more feedback on naming. - Updated API shape to access attributes/properties. - Added example to explore declarative version further. - Added clarification and example workaround for dynamic attribute/mixins changes. - Expanded section on the (future) possibility of conflicting mixins being used together and strategies to mitigate conflicts.
1 parent 2ce03ce commit 788e1ce

1 file changed

Lines changed: 47 additions & 13 deletions

File tree

PlatformProvidedMixins/explainer.md

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,6 @@ console.log(this._internals.mixins);
8989
- `ElementInternals` instances expose a read-only `mixins` property returning the list of attached behaviors.
9090
- Supports composition as web authors can pass many behaviors.
9191

92-
*Note: The API uses the term "mixins" instead of "behaviors" to avoid potential confusion with the spelling of "behavior" vs "behaviour".*
93-
9492
### Platform-Provided Behavior Mixins
9593

9694
The platform would expose the following behavior mixin, mirroring the submission capability of `HTMLButtonElement`:
@@ -110,7 +108,7 @@ Each platform behavior mixin must provide:
110108

111109
Platform-provided mixins expose useful public properties and methods of their corresponding native elements. Authors can expose these capabilities on their custom element's public API by defining accessors that delegate to the mixin state.
112110

113-
These APIs are accessed via specific properties on `ElementInternals` (e.g., `htmlSubmitButtonMixinState`). This property provides a direct interface to the underlying platform behavior managed by the mixin. It returns a non-null state object only if the corresponding mixin is passed to `attachInternals`.
111+
These APIs are accessed via the `mixins` property on `ElementInternals`. This property provides access to their instance-specific state via properties named after the mixin.
114112

115113
For `HTMLSubmitButtonMixin`, the state object exposes the following properties found on [`HTMLButtonElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLButtonElement):
116114

@@ -134,33 +132,40 @@ class CustomSubmitButton extends HTMLElement {
134132
}
135133

136134
get disabled() {
137-
return this._internals.htmlSubmitButtonMixinState.disabled;
135+
return this._internals.mixins.htmlSubmitButtonMixin.disabled;
138136
}
139137

140138
set disabled(val) {
141-
this._internals.htmlSubmitButtonMixinState.disabled = val;
139+
this._internals.mixins.htmlSubmitButtonMixin.disabled = val;
142140
}
143141

144142
get formAction() {
145-
return this._internals.htmlSubmitButtonMixinState.formAction;
143+
return this._internals.mixins.htmlSubmitButtonMixin.formAction;
146144
}
147145

148146
set formAction(val) {
149-
this._internals.htmlSubmitButtonMixinState.formAction = val;
147+
this._internals.mixins.htmlSubmitButtonMixin.formAction = val;
150148
}
151149
}
152150
```
153151

154-
This ensures developers don't have to reimplement the state logic that the mixin is supposed to provide.
152+
This ensures web authors don't have to reimplement the state logic that the mixin is supposed to provide.
155153

156154
### Composition via attachInternals
157155

158156
Passing behaviors to `attachInternals()` provides several advantages for web component authors:
159157

160158
- Behaviors are defined once during initialization, avoiding the complexity of managing behavior lifecycle (adding/removing) and state synchronization.
161159
- Authors can define a single class that handles multiple modes (submit, reset, button) by checking attributes before attaching internals, without needing to define separate classes for each behavior.
162-
- While this proposal focuses on an imperative API, the underlying model of attaching mixins via `ElementInternals` is compatible with future declarative APIs.
163160
- A child class extends the parent's functionality and retains access to the `ElementInternals` object and its active mixins, allowing for standard object-oriented extension patterns.
161+
- While this proposal uses an imperative API, the design supports future declarative custom elements. Once a declarative syntax for `ElementInternals` is established, attaching mixins could be modeled as an attribute, decoupling behavior from the JavaScript class definition. Platform-provided mixins could be referenced by string identifiers in markup. The following snippet shows a hypothetical example of declarative usage and how platform-provided mixins could be attached via an attribute.
162+
163+
```html
164+
<custom-button name="custom-submit-button">
165+
<element-internals mixins="html-submit-button-mixin"></element-internals>
166+
<template>Submit</template>
167+
</custom-button>
168+
```
164169

165170
### Use case: Design system button
166171

@@ -234,7 +239,29 @@ The element gains:
234239
- Form submission on activation.
235240
- `:default` pseudo-class matching.
236241
- Participation in implicit form submission.
237-
- Ability to inspect its own behaviors via `this._internals.mixins`.
242+
- Ability to inspect its own properties via `this._internals.mixins`.
243+
244+
*Note: In this proposal, the set of mixins is fixed when `attachInternals` is called. If the `type` attribute changes later, the element cannot dynamically swap mixins on the existing instance. To support dynamic type changes, web developers create a fresh instance of the element (which hasn't called `attachInternals` yet) with the new attribute and replace the old instance.*
245+
246+
```javascript
247+
attributeChangedCallback(name, oldVal, newVal) {
248+
// Only recreate if we've already initialized (mixins are locked).
249+
if (name === 'type' && this._internals) {
250+
// Create a fresh instance. It hasn't run connectedCallback yet,
251+
// so it hasn't called attachInternals.
252+
const replacement = document.createElement(this.localName);
253+
254+
// Copy the new type to the new instance.
255+
// The new instance will read this 'type' during its initialization.
256+
replacement.setAttribute('type', newVal);
257+
258+
// Swap the old element with the new one.
259+
// This triggers connectedCallback() on the new instance, allowing it
260+
// to call attachInternals() with the new mixin set.
261+
this.replaceWith(replacement);
262+
}
263+
}
264+
```
238265

239266
## Future Work
240267

@@ -248,7 +275,13 @@ While this proposal focuses on form submission, the mixin pattern can be extende
248275
- **Radio Groups**: `HTMLRadioGroupMixin` for `name`-based mutual exclusion.
249276
- **Tables**: `HTMLTableMixin` for table layout semantics and accessibility.
250277

251-
*Conflict Resolution: As the number of available mixins grows, we must address how to handle collisions when multiple mixins attempt to control the same attributes or properties. We propose that the order of mixins in the array passed to `attachInternals` should determine precedence (e.g., last one wins), but specific heuristics for complex clashes need to be defined.*
278+
### Conflict Resolution
279+
280+
As the number of available mixins grows, we must address how to handle collisions when multiple mixins attempt to control the same attributes or properties. We should explore several strategies to make composition possible without getting unexpected or conflicting behaviors:
281+
282+
1. **Order of Precedence (Default)**: The order of mixins in the array passed to `attachInternals` determines precedence (e.g., "last one wins"). This is simple to implement but may hide subtle incompatibilities.
283+
2. **Compatibility Allow-lists**: Each mixin could define a short list of "compatible" mixins that can be used in combination. Any combination not explicitly allowed would be rejected by `attachInternals`, preventing invalid states (like being both a button and a form).
284+
3. **Explicit Conflict Resolution**: If conflicts occur, the platform could require the author to explicitly alias or exclude specific properties.
252285

253286
### Future use case: Inheritance and composition
254287

@@ -433,7 +466,7 @@ customElements.define('fancy-button', FancyButton, { extends: 'button' });
433466

434467
While customized built-ins are useful where supported, the issues listed above makes them unsuitable as the primary solution.
435468

436-
### Alternative 5: Expose certain behavioural attributes via ElementInternals (Proposed)
469+
### Alternative 5: Expose certain behavioral attributes via ElementInternals (Proposed)
437470

438471
Expose specific behavioral attributes (like `popover`, `draggable`, `focusgroup`) via `ElementInternals` so custom elements can adopt them without exposing the attribute to the user. See [issue #11752](https://github.com/whatwg/html/issues/11752).
439472

@@ -484,7 +517,7 @@ While valuable, this can be a parallel effort. Even if all native elements were
484517

485518
### Browser Vendors
486519

487-
- Chromium: No signal
520+
- Chromium: Positive
488521
- Gecko: No signal
489522
- WebKit: No signal
490523

@@ -498,6 +531,7 @@ Many thanks for valuable feedback and advice from:
498531
- [Hoch Hochkeppel](https://github.com/mhochk)
499532
- [Kevin Babbitt](https://github.com/kbabbitt)
500533
- [Kurt Catti-Schmidt](https://github.com/KurtCattiSchmidt)
534+
- [Mason Freed](https://github.com/mfreed7)
501535

502536
Thanks to the following proposals, articles, frameworks, and languages for their work on similar problems that influenced this proposal.
503537

0 commit comments

Comments
 (0)