Skip to content

Commit e4bb41c

Browse files
authored
Platform provided behaviors feedback 8 (#1283)
- Feature detection section - A couple of other clarifications to mention [Web Platform Design Principles](https://www.w3.org/TR/design-principles/) more explicitly - Update Open questions section: first question (automatic exposure of properties in the element) has been compacted and second question (dynamic behaviors) has been deleted.
1 parent 6e74300 commit e4bb41c

File tree

1 file changed

+31
-105
lines changed

1 file changed

+31
-105
lines changed

PlatformProvidedBehaviors/explainer.md

Lines changed: 31 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -101,11 +101,13 @@ submitBehavior?.disabled = true;
101101
102102
Platform behaviors give custom elements capabilities that would otherwise require reimplementation or workarounds. Each behavior automatically provides:
103103
104-
- Event handling: Platform events (click, keydown, etc.) are wired up automatically.
104+
- Event handling: Platform events (click, keydown, etc.) are wired up automatically using the standard DOM event infrastructure (respecting `stopPropagation`, `preventDefault`, etc.)
105105
- ARIA defaults: Implicit roles and properties for accessibility.
106106
- Focusability: The element participates in the tab order as appropriate for the behavior.
107107
- CSS pseudo-classes: Behavior-specific pseudo-classes are managed by the platform.
108108
109+
By bundling these capabilities as high-level units, the platform can ensure accessible defaults, correct event wiring, and proper pseudo-class management.
110+
109111
This proposal introduces `HTMLSubmitButtonBehavior`, which mirrors the submission capability of `<button type="submit">`:
110112
111113
| Capability | Details |
@@ -196,7 +198,7 @@ When `attachInternals()` is called with behaviors, each behavior is attached to
196198
| Element disconnected from DOM | Behavior state is preserved. Event handlers remain conceptually attached but inactive. |
197199
| Element reconnected to DOM | Event handlers become active again. Behavior state (e.g., `formAction`, `disabled`) is preserved. |
198200
199-
*Note: Behaviors are immutable after `attachInternals()`. See the [open question on dynamic behaviors](#should-we-support-dynamic-behavior-updates).*
201+
*Note: Behaviors are immutable after `attachInternals()`. Dynamic behavior updates (adding, removing, or replacing behaviors after attachment) are not supported, as developer feedback indicated that the problems with `<input>`'s mutable `type` attribute (state migration, event handler cleanup, property compatibility) should not be replicated.*
200202
201203
### Duplicate behaviors
202204
@@ -233,10 +235,10 @@ This ensures that element-specific properties like `behavior.form` and `behavior
233235
The current API uses instantiated behaviors with a single `behaviors` property:
234236
235237
- `behaviors` option in `attachInternals({ behaviors: [...] })` accepts behavior instances.
236-
- `behaviors` property on `ElementInternals` is a read-only array.
238+
- `behaviors` property on `ElementInternals` is a read-only `FrozenArray`.
237239
- Developers hold direct references to their behavior instances.
238240
239-
*Note: An array is preferred over a set because order may be significant for [conflict resolution](#behavior-composition-and-conflict-resolution). A set provides no ordering guarantees, which would make conflict resolution unpredictable.*
241+
*Note: An ordered array is preferred over a set because order may be significant for [conflict resolution](#behavior-composition-and-conflict-resolution). `behaviors` uses a `FrozenArray` because behaviors are immutable after attachment.*
240242
241243
**Pros:**
242244
- Single property name.
@@ -285,7 +287,7 @@ this._internals.behaviors.htmlSubmitButton.formAction = '/custom';
285287
- Less setup code as developers don't manage behavior instances.
286288
287289
**Cons:**
288-
- Platform instantiates the behavior, so constructor parameters aren't available.
290+
- Platform instantiates the behavior, so constructor parameters aren't available. This conflicts with the [design principle that classes should have constructors](https://www.w3.org/TR/design-principles/#constructors) that allow authors to create and configure instances.
289291
- Requires a `behaviors` interface for named access.
290292
- *Future* developer-defined behaviors would need a way to name their behaviors.
291293
@@ -494,6 +496,24 @@ class LabeledSubmitButton extends HTMLElement {
494496
- Adds complexity for simple cases where order-based resolution would suffice.
495497
- Authors must understand all potential conflicts to resolve them correctly.
496498
499+
### Feature detection
500+
501+
Web authors can detect whether behaviors are supported by checking for the existence of behavior classes on the global scope:
502+
503+
```javascript
504+
if (typeof HTMLSubmitButtonBehavior !== 'undefined') {
505+
// Behaviors are supported.
506+
this._submitBehavior = new HTMLSubmitButtonBehavior();
507+
this._internals = this.attachInternals({ behaviors: [this._submitBehavior] });
508+
} else {
509+
// Fall back to manual event handling.
510+
this._internals = this.attachInternals();
511+
this.addEventListener('click', () => {
512+
this._internals.form?.requestSubmit(this);
513+
});
514+
}
515+
```
516+
497517
### Other considerations
498518
499519
This proposal supports common web component patterns:
@@ -821,108 +841,14 @@ Although this proposal currently focuses on custom elements, the behavior patter
821841
822842
### Should behavior properties be automatically exposed on the element?
823843
824-
The current proposal requires developers to manually create getters/setters that delegate to the stored behavior instance (e.g., `this._submitBehavior.*`). There are alternative approaches worth considering:
825-
826-
#### Option A: Manual property delegation (current proposal)
827-
828-
**Pros:**
829-
- Authors have full control over their element's public API.
830-
- No naming conflicts.
831-
- Authors can add validation, transformation, or side effects in setters.
832-
- Familiar pattern.
833-
834-
**Cons:**
835-
- Boilerplate for each property the author wants to expose.
836-
- For future behaviors like `HTMLInputBehavior`, not exposing `value` means external code can't read or set the input's data without the developer writing boilerplate getters and setters.
837-
838-
#### Option B: Automatic property exposure
839-
840-
The platform automatically adds behavior properties to the custom element:
841-
842-
```javascript
843-
class CustomSubmitButton extends HTMLElement {
844-
constructor() {
845-
super();
846-
this._submitBehavior = new HTMLSubmitButtonBehavior();
847-
this.attachInternals({ behaviors: [this._submitBehavior] });
848-
}
849-
}
850-
851-
const btn = document.createElement('custom-submit');
852-
// Works without any getter/setter.
853-
btn.disabled = true;
854-
btn.formAction = '/save';
855-
```
856-
857-
**Pros:**
858-
- Zero boilerplate code to get and set properties.
859-
- Matches how native elements work (a `<button>` just has `disabled`).
860-
- For future behaviors like `HTMLCheckboxBehavior` external code can read/write `checked` without the developer writing any delegation code.
861-
862-
**Cons:**
863-
- Naming conflicts if the element already defines a property with the same name.
864-
- Less control over the public API surface.
865-
- Authors can't easily add validation or side effects to setters.
866-
- May feel "magical" compared to explicit delegation.
867-
- Unclear behavior when a behavior is removed. If `formAction` was automatically exposed when `HTMLSubmitButtonBehavior` was attached, what happens to that property after the behavior is removed? Does it remain on the element with a stale value, or is it removed?
868-
869-
```javascript
870-
const btn = document.createElement('custom-submit');
871-
btn.formAction = '/save';
872-
btn.removeBehavior();
873-
btn.formAction; // Is it still there?
874-
```
875-
876-
#### Option C: Opt-in automatic exposure
877-
878-
A middle ground where authors can choose:
879-
880-
```javascript
881-
// Explicit delegation (default).
882-
this._submitBehavior = new HTMLSubmitButtonBehavior();
883-
this.attachInternals({ behaviors: [this._submitBehavior] });
884-
885-
// Opt-in to automatic exposure.
886-
this._submitBehavior = new HTMLSubmitButtonBehavior();
887-
this.attachInternals({
888-
behaviors: [this._submitBehavior],
889-
exposeProperties: true // or list specific properties.
890-
});
891-
```
892-
893-
**Pros:**
894-
- Flexibility: authors choose the right approach for their use case.
895-
- Backwards compatible with explicit delegation.
896-
897-
**Cons:**
898-
- More complex API.
899-
- Still needs to handle naming conflicts when `exposeProperties` is enabled.
900-
901-
#### Why this matters
902-
903-
Future behaviors would likely require developers to expose certain properties for the element to be useful to consumers. Without developer-written delegation, external JavaScript code can't access these properties:
904-
905-
| Behavior | Key property | Impact if not exposed |
906-
|----------|------------------|------|
907-
| `HTMLCheckboxBehavior` | `checked` | The behavior toggles internal state on click, but external scripts can't read or set it. |
908-
| `HTMLInputBehavior` | `value` | External scripts can't programmatically read the input's data or populate it. (Form submission would still work via `setFormValue()`.) |
909-
| `HTMLRadioGroupBehavior` | `checked` | Mutual exclusion happens internally, but external scripts can't query which radio is selected. |
910-
911-
If `HTMLSubmitButtonBehavior` uses manual delegation but `HTMLCheckboxBehavior` uses automatic exposure, we'd have an inconsistent API surface. This argues for deciding on a consistent approach across all behaviors from the start.
912-
913-
### Should we support dynamic behavior updates?
914-
915-
This proposal uses static behaviors: once attached via `attachInternals()`, behaviors cannot be added, removed, or replaced. One argument to support dynamic behavior updates is to mirror native `<input>` element flexibility, where changing the `type` attribute switches between radically different behaviors (text field → checkbox → date picker). However, feedback suggests that `<input>`'s design shouldn't be emulated:
916-
917-
- The `type` attribute fundamentally changes what the element is.
918-
- Different input types have incompatible properties (`checked` vs `value` vs `files`).
919-
- The design makes `<input>` difficult to style and reason about.
844+
The current proposal uses manual property delegation: developers create getters/setters that delegate to the stored behavior instance. This gives authors full control over their public API, avoids naming conflicts, and allows validation or side effects in setters. The tradeoff is boilerplate for each exposed property.
920845
921-
*See [Monica Dinculescu's analysis](https://meowni.ca/posts/a-story-about-input/) documenting the problems with `<input>`.*
846+
Two alternatives have been considered:
922847
923-
For behaviors, the same problems would apply: if behaviors could be swapped dynamically, authors would need to handle state migration, event handler cleanup, and property compatibility.
848+
- **Automatic property exposure:** The platform adds behavior properties directly to the custom element (e.g., `btn.disabled = true` works without any getter/setter). This matches how native elements work but introduces naming conflicts, reduces API control, and feels "magical."
849+
- **Opt-in automatic exposure:** Authors choose per-element whether properties are auto-exposed via an option like `exposeProperties: true`. This offers flexibility but adds API complexity.
924850
925-
If compelling use cases emerge that genuinely require dynamic behavior composition, the API could be extended to use `ObservableArray` with lifecycle callbacks. This would be a backwards-compatible change (making the array mutable doesn't break code that treats it as read-only). However, we believe static behaviors will cover the vast majority of real-world needs.
851+
Future behaviors like `HTMLCheckboxBehavior` or `HTMLInputBehavior` would require developers to write boilerplate for key properties (`checked`, `value`) that external code needs to access. A consistent approach across all behaviors should be decided before additional behaviors ship.
926852
927853
### Is there a better name than "behavior" for this concept?
928854
@@ -1089,7 +1015,7 @@ Expose individual primitives (focusability, disabled, keyboard activation) direc
10891015
10901016
**Cons:**
10911017
- Primitives like `disabled` and `focusable` interact with each other, with accessibility, and with event handling. Setting `internals.disabled = true` without the associated behavior might result in the element *looking* disabled but still receiving clicks, remaining in the tab order, and submitting with a form.
1092-
- Even seemingly simple primitives like focusability could have significant complexity around accessibility integration. This is why `popovertarget` is limited to buttons(it was originally intended for any element, but the accessibility requirements around focusability and activation made buttons the practical choice).
1018+
- Even seemingly simple primitives like focusability could have significant complexity around accessibility integration. This is why `popovertarget` is limited to buttons(it was originally intended for any element, but the accessibility requirements around focusability and activation made buttons the practical choice). See [design-principles tradeoff between high-level and low-level APIs](https://www.w3.org/TR/design-principles/#high-level-low-level).
10931019
- Form submission participation can be seen as a primitive itself (it can't be broken down further due to accessibility concerns).
10941020
10951021
### Alternative 8: TC39 Decorators

0 commit comments

Comments
 (0)