@webfeet/reflect
v0.1.0
Published
Helper classes implementing HTML attribute reflection rules for use in custom elements
Readme
@webfeet/reflect
Implements the HTML attribute reflection rules
TODO
Currently, reflection is implemented for IDL attributes of type DOMString, DOMString?, USVString representing an URL, boolean, long, unsigned long, double, element, and frozen array of elements are implemented,
no DOMTokenList (technically impossible, but could be approximated).
API
This package exports constants and functions whose name starts with reflect and whose value or the value they return, collectively known as reflectors, is an object with three functions and one constant:
fromAttributetakes a string ornullas its single argument, coming directly from an HTML attribute, and implements the getter steps of the reflection rules, returning a value of the appropriate typecoerceValuetakes any value as its single argument, and coerces it to the appropriate type following the WebIDL rules; it's generally directly one of thecoerceTofunctions exported by @webfeet/webidlsetAttributetakes three arguments: the HTML element, the attribute name, and the attribute's value (of the same type as returned bycoerceValue), and implements the setter steps of the reflection rulesdefaultValueis the default value one should initialize a property to behave as if an element had no HTML attribute (in other words, this is the value that will be returned byfromAttribute(null))
Those functions aren't methods of the returned object, they don't rely on their this, so they can be stored in variables and then directly called.
Enumerated attributes (and nullable enumerated attributes) take options as properties of an object passed to the exported functions:
keywordsis the list of canonical keywords (the ones that can be returned fromfromAttribute)aliasesis an object mapping a non-canonical keyword to a canonical keywordmissingrepresents the missing default value, that will be thedefaultValueand will be returned byfromAttribute(null)invalidrepresents the invalid default value, that will be returned byfromAttributewhen the value doesn't match any known keyword (canonical or alias)
Only the keywords option is mandatory, all others are optional.
Numbers take options as properties of an object passed to the exported function. Most numbers only take a single, optional, option: defaultValue, the default value used when the attribute is absent or cannot be parsed to a number.
Clamped integers also take two mandatory options in addition to the optional defaultValue: min and max represent the bounds of the range the value is clamped to.
The reflectURL constant is slightly different from all the others, as it doesn't have a defaultValue and its fromAttribute takes two arguments: the HTML element, and then a string or null as the value coming from the HTML attribute. This is because the value of a USVString attribute representing a URL relies on the document's base URL, which can change at any time.
Finally, reflectElementReference and reflectElementReferences are factories of stateful reflectors, whose API is a bit different from the other stateless reflectors.
The factory function itself takes the element as the first argument, and optionally an element type (e.g. HTMLDivElement) to filter the elements referenced by ID from the attribute. If not given, the second argument defaults to Element.
The returned stateful reflector is an object with internal state and 4 functions:
gettakes no argument, and implements the getter steps of the reflection rules, computing and returning the referenced element(s). ForreflectElementReference, the returned value is either an element of the type passed to the constructor, ornull. For thereflectElementReferences, the value is a frozen array of elements of the type passed to the constructor, ornull.fromAttributetakes a string ornullas its single argument, coming directly from an HTML attribute, and updates the reflector's internal state, not returning anything.coerceValuetakes any value as its single argument, and coerces it to the appropriate type (same asget).setAttributetakes two arguments: the attribute name, and the attribute's value (of the same type as returned bycoerceValue), and implements the setter steps of the reflection rules.
A property backed by reflectElementReference should have its name suffixed with Element (relative to the HTML attribute name).
A property backed by reflectElementReferences should have its name suffixed with Elements.
Usage
The (stateless) reflectors are meant to be used in two possible ways (except for reflectURL):
either applying the getter steps in the property getter, as specificied in HTML
class MyElement extends HTMLElement { get prop() { return reflector.fromAttribute(this.getAttribute("prop")); } set prop(value) { value = reflector.coerceValue(value); reflector.setAttribute(this, "prop", value); } }or applying the getter steps whenever the attribute changes, and caching the result (this approach is not compatible with
reflectURL)class MyElement extends HTMLElement { #prop = reflector.defaultValue; static observedAttributes = ["prop"]; attributeChangedCallback(name, oldValue, newValue) { // in this case, we know 'name' is necessarily 'prop' so we can skip any check this.#prop = reflector.fromAttribute(newValue); } get prop() { return this.#prop; } set prop(value) { value = reflector.coerceValue(value); reflector.setAttribute(this, "prop", value); } }
The reason the coerceValue and setAttribute are separated is to allow for custom steps to be added in between. coerceValue should always be the first thing called in the setter to coerce the input value, and setAttribute will most likely always be the last.
The stateful reflectors however require the use of attributeChangedCallback:
class MyElement extends HTMLElement {
#prop = reflectElementReference(this);
static observedAttributes = ["prop"];
attributeChangedCallback(name, oldValue, newValue) {
// in this case, we know 'name' is necessarily 'prop' so we can skip any check
this.#prop.fromAttribute(newValue);
}
get propElement() {
return this.#prop.get();
}
set propElement(value) {
value = this.#prop.coerceValue(value);
this.#prop.setAttribute("prop", value);
}
}