-
Notifications
You must be signed in to change notification settings - Fork 386
Description
This proposal introduces an API for defining custom attributes for both built-ins and custom elements, and design discussion for an API that can be used to define more complex enhancements that involve multiple attributes, methods, JS-only properties etc.
This came out of the TPAC extending built-ins breakout, and some follow-up discussions with @keithamus.
Defining attributes: The Attribute
class
Use cases
- Augmenting existing elements (either built-ins or custom elements) with custom attributes that trigger custom behaviors
- Easier definition of a web component's own attributes, as this automatically takes care of property-attribute reflection, typing, default values etc, one of the biggest pain points of defining WCs without helpers
Prior art
- Original custom attributes proposal (naming only)
- @lume's custom attributes proposal
- LitElement properties
- VueJS props
Needs
- Lifecycle hooks: connected, disconnected, changed
- Default values
- Optional typing
API sketch
- Subclasses that extend a base Attribute class
- Static
HTMLElement.attributeRegistry
property which is an AttributeRegistry object. Attributes can be registered generically onHTMLElement
to be available everywhere, or on specific classes (built-in or custom element classes).- For non custom elements, attribute names MUST contain a hyphen and not match a blacklist (
aria-*
, SVG attributes etc). Or maybe this should be a validation restriction, and not actually enforced by the API?
- For non custom elements, attribute names MUST contain a hyphen and not match a blacklist (
Attribute
should extendEventTarget
so that authors can dispatch events on it.
Definition:
class ListAttribute extends Attribute {
ownerElement; // element this is attached to
value; // current value
connectedCallback() { /* ... */ }
disconnectedCallback() { /* ... */ }
// Called whenever the attribute's value changes
changedCallback() { /* ... */ }
static dataType = AttributeType.IDREF;
// Optional default value
static defaultValue = null;
}
Usage on built-ins or existing custom elements:
HTMLInputElement.attributeRegistry.define("ac-list", ListAttribute);
An optional options dictionary allows customizing the registration, such as:
propertyName
to override the automatic camelCase conversion
MyInput.attributeRegistry.define("ac-list", ListAttribute, {
propertyName: "autocompleteList",
});
Usage on new custom elements:
class MyInput extends HTMLElement {
...
}
MyInput.attributeRegistry.define("ac-list", ListAttribute);
MyInput.attributeRegistry.define("value", class ValueAttribute extends Attribute {
dataType = AttributeType.NUMBER;
defaultValue = 0;
});
We could also add a new static attributes
property as syntactic sugar to simplify the code needed and keep the definition within the class:
class MyInput extends HTMLElement {
// Or maybe an array?
static attributes = {
"ac-list": ListAttribute,
// Creates a suitable Attribute subclass behind the scenes:
value: { dataType: AttributeType.NUMBER, defaultValue: 0 }
}
}
Types
In v0 types could only be predefined AttributeType
objects, in the future these should be constructible (all they need is a parse()
and stringify()
function, and maybe an optional defaultValue
to act as a base default value).
Open Questions
- Should lifecycle hooks be called when the attribute is added, or when the element is connected/disconnected?
Complex Enhancements
Complex enhancements include:
- Multiple attributes (references to
Attribute
objects) - Methods
- Properties and accessors (that don't correspond to attributes)
This can be fleshed out at a later time, since Attribute
already deals with a lot of use cases. That could give us time to get more data about what is needed.
Prior art
Needs & Design discussion
Referencing: How to associate enhancements with elements?
Element behaviors use a has
attribute that takes a list of identifiers, and that is totally a viable path. The downside of this is that it introduces noise, and potential for error. E.g. imagine implementing a language like VueJS in this way, one would need to use has
in addition to any v-*
attribute, and would inevitably forget.
Associating enhancements with a selector would probably afford maximum flexibility and allows the association to happen implicitly (e.g. for an Enhancement implementing htmx, the "activation selector" could be [hx-get], [hx-post], [hx-swap], ...
, without an additional has="htmx"
being required eveyrwhere that these attributes are used.
Imperative association could allow us to explore functionality without committing to a particular declarative scheme. That way, individual attributes can still automatically activate behaviors, though it's a bit clunky:
class MyAttribute extends Attribute {
connectedCallback () {
this.ownerElement.behaviors.add(Htmx);
}
disconnectedCallback () {
let hasOthers = this.ownerElement.getAttributeNames().some(n => n.startsWith("hx-");
if (!hasOthers) this.ownerElement.behaviors.delete(Htmx);
}
}
Flexibility
Should enhancements allow the same separation of names and functionality as custom elements and attributes? Given that they include multiple attributes, how would the association happen? I'm leaning towards that they'd also name the attributes they are including (since they are naming properties, methods etc already anyway).
// Should have the same syntax as HTMLElement.attributes above
attributes: {
"v-if": VIf
"v-else": VElse,
"v-on": { value: VOn, propertyName: "vOnEvent" }
...
}
List of use cases
I have been maintaining a list of use cases that I periodically add to here, I will edit this periodically to import it:
Use cases for custom attributes
- Emulating native functionality like…
- datalist for autocomplete
- title attribute for tooltips
- Popover API
- Invokers
format
attribute on<time>
,<data>
for presenting data with custom formats- Form controls
- Custom form validation
- Show/hide password
- Checkboxes
- Designate a checkbox as an aggregate checkbox for checkboxes with a given
name
- Designate a checkbox as an aggregate checkbox for checkboxes with a given
- Text inputs
- Specific format, e.g. currency, measurement, 5-digit code, etc
<button>
<button href>
- General
- Add icon (
prefix="icon-name"
/suffix="icon-name"
) - Skeleton (e.g.
<p loading-placeholder="3 sentences">
) - Tooltip
- Data binding
- Highlight matches within (e.g.
highlight="foobar"
) removable
(adds X button that removes the element and fires a suitable event)
- Add icon (
- Tables
<table sortable>
<td value>
to be used for filtering and sorting
<pre>
<pre src>
<pre editable>
for code editors (or any other element)<pre normalize-whitespace>
- HTMLMediaElement (
<audio>
and<video>
)- Custom toolbar
start-at="0:05"