-
Notifications
You must be signed in to change notification settings - Fork 251
[Custom Elements with Native Element Behaviors] Other considered alternatives #1135
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -250,6 +250,130 @@ A partial solution for this problem already exists today. Authors can specify th | |||||
|
||||||
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. | ||||||
|
||||||
### Compositional Mixins via `elementInternals.addMixin()` | ||||||
|
||||||
This alternative proposes a compositional API that allows web developers to opt into specific native behaviors using mixins. These can be added via a method like `elementInternals.addMixin()`, injecting native-like capabilities (e.g., form participation, activation behavior) into custom elements. The approach supports both built-in mixins (provided by the platform) and user-defined ones, enabling flexible combinations of behaviors. | ||||||
|
||||||
```js | ||||||
function MyCustomBehaviorMixin(Base) { | ||||||
return class extends Base { | ||||||
connectedCallback() { | ||||||
super.connectedCallback?.(); | ||||||
this.setAttribute('data-enhanced', 'true'); | ||||||
this.addEventListener('mouseover', () => { | ||||||
this.style.backgroundColor = 'lightblue'; | ||||||
}); | ||||||
} | ||||||
}; | ||||||
} | ||||||
|
||||||
class CustomButton extends HTMLElement { | ||||||
constructor() { | ||||||
super(); | ||||||
const internals = this.attachInternals(); | ||||||
// Add browser built-in behavior for activation | ||||||
internals.addMixin(ButtonActivationMixin); | ||||||
// Add custom behavior | ||||||
internals.addMixin(MyCustomBehaviorMixin); | ||||||
} | ||||||
} | ||||||
customElements.define('custom-button', CustomButton); | ||||||
``` | ||||||
|
||||||
#### ButtonActivationMixin (Built-in Mixin) | ||||||
|
||||||
A browser-implemented mixin that encapsulates the native behavior of a element. When applied, it enables: | ||||||
|
||||||
- Keyboard activation (e.g., triggering on `Enter` or `Space`). | ||||||
- Click handling and dispatching of click events. | ||||||
- Participation in form submission if applicable. | ||||||
- All properties and attributes from the native element's implementation. | ||||||
- Accessibility roles and ARIA integration. | ||||||
|
||||||
This mixin would be part of the platform, with the goal of ensuring consistent behavior across custom elements that opt into it. | ||||||
|
||||||
#### MyCustomBehaviorMixin (User-defined Mixin) | ||||||
|
||||||
A web author-implemented mixin that adds custom logic or features to an element. In the sample code above, it: | ||||||
|
||||||
- Adds a data-enhanced attribute when the element is connected. | ||||||
- Changes the background color on mouseover. | ||||||
- Demonstrates how web developers can encapsulate reusable behaviors. | ||||||
|
||||||
Compositional Mixins via `elementInternals.addMixin()` has the following disadvantages: | ||||||
|
||||||
- **Blurs the boundary of built-in elements**: Custom elements could combine capabilities from multiple built-in elements (e.g., behave like both a button and a label), potentially introducing confusion in expected behavior. The platform may need more concrete real-world use cases before moving in this direction. | ||||||
- **Implicit behavior flags still needed**: Some behaviors (e.g., native button or label functionality) still require implicit flags to activate. This reintroduces the need for a static property like `static behavesLike = 'button'`, which the mixin model aimed to avoid. | ||||||
|
||||||
### Compositional Mixins via Subclass Factories | ||||||
|
||||||
This alternative proposes leveraging JavaScript subclass factories to compose native-like behaviors into custom elements. This approach aligns with existing JavaScript patterns and avoids introducing new platform-level APIs. | ||||||
Web authors would use mixin functions that return subclasses of `HTMLElement`, each encapsulating a specific behavior (e.g., form association, activation). These mixins could be layered to create elements with multiple built-in capabilities. | ||||||
|
||||||
```js | ||||||
function ButtonCustomMixin(Base) { | ||||||
return class extends Base { | ||||||
static get observedAttributes() { | ||||||
return ['command']; | ||||||
} | ||||||
|
||||||
constructor() { | ||||||
super(); | ||||||
this._command = null; | ||||||
} | ||||||
|
||||||
get command() { | ||||||
return this._command; | ||||||
} | ||||||
|
||||||
set command(value) { | ||||||
this._command = value; | ||||||
this.setAttribute('command', value); | ||||||
} | ||||||
|
||||||
attributeChangedCallback(name, oldValue, newValue) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this subclass factory should contain all properties/methods from the built-in Button element, not attributeChangedCallback, connectedCallback, etc which are for custom elements |
||||||
if (name === 'command') { | ||||||
this.command = newValue; | ||||||
} | ||||||
} | ||||||
|
||||||
connectedCallback() { | ||||||
if (this.hasAttribute('command')) { | ||||||
this.command = this.getAttribute('command'); | ||||||
} | ||||||
|
||||||
this.addEventListener('click', (e) => { | ||||||
if (this.command) { | ||||||
// Custom behavior: dispatch a command event with the command name | ||||||
this.dispatchEvent(new CustomEvent('custom-command', { | ||||||
detail: { name: this.command }, | ||||||
bubbles: true, | ||||||
composed: true | ||||||
})); | ||||||
} | ||||||
}); | ||||||
} | ||||||
}; | ||||||
} | ||||||
|
||||||
class CustomButton extends ButtonCustomMixin(HTMLButtonElement) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
ButtonInternalMixin probably is a better name, and the base class should be HTMLElement, not HTMLButtonElement |
||||||
constructor() { | ||||||
super(); | ||||||
this.internals = this.attachInternals(); | ||||||
} | ||||||
} | ||||||
customElements.define('custom-button', CustomButton, { extends: 'button' }); | ||||||
``` | ||||||
|
||||||
In the sample code above, a custom command behavior is shown. Instead of linking to a `<command>` element (as in native HTML), this implementation dispatches a custom-command event when clicked, passing the command name in the event's detail. This pattern allows web authors to define their own command-handling logic elsewhere in the application, offering greater flexibility than the native model. | ||||||
|
||||||
Compositional Mixins via Subclass Factories has the following disadvantages: | ||||||
|
||||||
- **Blurs the boundary of built-in elements**: Similar to the compositional mixins via `elementInternals.addMixin()` alternative, custom elements could combine capabilities from multiple built-in elements. This may introduce ambiguity in behavior and expectations. | ||||||
- **Increased complexity for declarative usage**: Supporting mixins via subclass factories in declarative HTML (e.g., `<my-element behaves-like="button">`) would be significantly more complex than a single type string. | ||||||
- **Prototype chain manipulation**: While subclass factories are idiomatic in JavaScript, they can result in deep and complex prototype chains. This may complicate debugging, degrade performance, and hinder interoperability with platform features such as accessibility and form controls | ||||||
- **Unproven feasibility in the platform**: The subclass factory pattern has never been used in the web platform before. While it is common in userland JavaScript, we currently lack sufficient technical knowledge to confirm whether this approach is feasible or compatible with the platform’s internals. This introduces uncertainty about its viability. | ||||||
|
||||||
## Stakeholder Feedback / Opposition | ||||||
|
||||||
- Chromium : Positive | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a subclass factory, which is not this alternative is about. This alternative is more about an approach similar to ReactiveController