Skip to content

Proposal: Custom attributes for all elements, enhancements for more complex use cases #1029

@LeaVerou

Description

@LeaVerou

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

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 on HTMLElement 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?
  • Attribute should extend EventTarget 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
    • 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)
  • 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"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions