web-component-attributes
v0.1.1
Published
Tiny library to link HTML attributes with JS properties on Web Components
Readme
web-component-attributes
a tiny library to link HTML attributes with JS properties on Web Components
Rationale
The Web Components API allows you to use custom HTML attributes on your components, and provides a simple, flexible way to observe changes in their values. While this offers a lot of power in choosing how exactly your component should respond to attributes, it doesn't integrate the attributes seamlessly with Javascript the same way most built-in HTML attributes do.
As an example, take the id attribute. If you include an element
<my-element id="foo"></my-element> in your HTML, you can access its id in JS in either of two
ways: myElement.getAttribute('id') or myElement.id. Most devs choose the latter. The property
access style is less verbose and therefore easier to read, but more importantly, it better
represents the way we think about the operation. An element's id is, conceptually, one of many
properties it may have as an object. When you access the id, what you're really doing is querying
a key-value map of the element's attributes, but what it feels like you're doing is a simple
property access.
In order to replicate this functionality for a custom attribute, the component needs to define a getter and setter for a property which acts as a proxy for the underlying HTML attribute. This isn't difficult, but it is boilerplate that would be nice to avoid. This package provides an alternative by programmatically creating getters and setters from a declarative description of an attribute.
Usage
The web-components-attributes package provides a single default export, the function
WebComponentAttributesMixin(Component, attributes):
// Import the package
import WebComponentAttributesMixin from 'web-component-attributes';This function takes a Web Component class Component as its first argument, and an object
attributes describing the attributes to declare as its second argument. Component must be a
Javascript class, such as one defined with the ES6 class syntax, which inherits from HTMLElement
or one of its subclasses. The function is a mixin, meaning that it modifies the class it receives as
its argument in order to add extra functionality to it. It's intended to be used like so:
class MyWebComponent extends HTMLElement {
/* ... add some custom behavior ... */
}
// Set up properties which act as proxies for HTML attributes
WebComponentAttributesMixin(MyWebComponent, {
/* ... attribute definitions ... */
});
customElements.define('my-web-component', MyWebComponent);Attribute Definition Format
The attributes argument to WebComponentAttributesMixin takes the form of an object which acts as
a key-value map. Each key (property name) is the name of a proxy property as you would spell it when
using obj.prop accessor syntax in Javascript. Each corresponding value is an object which lists a
type, a default value, and an htmlName:
WebComponentAttributesMixin(MyWebComponent, {
myStringProperty: {
type: 'string',
default: "foo bar",
htmlName: "my-string-attribute",
},
myNumberProperty: {
type: 'number',
default: 6.28,
htmlName: "my-number-attribute",
},
});type determines the type of the JS property. All HTML attributes are stored as strings; when you
get the value of the property, the getter will attempt to convert the string value to a value of the
appropriate type, and when you set the value of the property, the new value will be stored in the
backing attribute in a string representation. This is similar to the way numeric attributes like
<input>'s size work.
A complete list of supported types is in the Types section below.
default determines the default value of the JS property if the corresponding HTML attribute
doesn't exist on the element or if the string value of the attribute can't be parsed into a valid
value of the type. Custom elements created with the class constructor or with
document.createElement(...) start without any attributes, so this will also be the value of the
property when the element is first created. Note that it's possible for an attribute to be present
but have its value as the empty string, in which case the getter will attempt to parse the empty
string as a value of the type.
The default value may be of a different type than the listed type. This allows you to use
sentinel values (e.g. null) to indicate the lack of a valid value.
htmlName determines the name of the attribute as it appears in HTML. This is also the name you'd
use when accessing the attribute via getAttribute(...) and related methods.
If you used the example above to define .myStringProperty and .myNumberProperty proxies on
MyWebComponent, you'd be able to use them as follows:
// x1 and x2 will be the same:
const x1 = myWebComponent.myStringProperty;
const x2 = myWebComponent.hasAttribute('my-string-attribute')
? myWebComponent.getAttribute('my-string-attribute')
: "foo bar";
// This sets the HTML attribute my-string-attribute to "baz":
myWebComponent.myStringProperty = "baz";
// y1 and y2 will be the same:
const y1 = myWebComponent.myNumberProperty;
const y2 = myWebComponent.hasAttribute('my-number-attribute')
? parseFloat(myWebComponent.getAttribute('my-number-attribute'))
: 6.28;
// This sets the HTML attribute my-number-attribute to "100":
myWebComponent.myNumberProperty = 100;Other Attribute Definition Formats
In some cases you can use a shorthand version of the attribute declaration format, instead of
writing out the full type, default, and htmlName.
If htmlName is omitted, it will be generated from the name of the JS property. This will convert
the property's camel case name to a kebab case attribute name. The attribute name will always be in
all lowercase letters.
WebComponentAttributesMixin(MyWebComponent, {
someOptionalStringProperty: {
type: 'string',
default: null,
// htmlName generated as "some-optional-string-property"
},
});If default is omitted, the default value of the property will be inferred to be null. The
attributes specifiesNullDefault and nullDefaultInferred will thus behave identically:
WebComponentAttributesMixin(MyWebComponent, {
specifiesNullDefault: {
type: 'string',
default: null,
htmlName: "specifies-null-default",
},
nullDefaultInferred: {
type: 'string',
htmlName: "null-default-inferred",
},
});If, instead of an object containing type, default, and htmlName fields, you use a primitive
value, default will be set to that value, type will be inferred from the value's type, and
htmlName will be generated from the property name. The attributes explicit and implicit will
thus behave identically:
WebComponentAttributesMixin(MyWebComponent, {
explicit: {
type: 'string',
default: "foo bar",
},
implicit: "foo bar",
});Strings, Numbers, BigInts, and Booleans will be inferred as type 'string', 'number',
'bigint', and 'boolean', respectively.
Types
This section is organized into subsections based on the value of the type field of an attribute.
'string'
The getter accesses the raw string data of the attribute. The setter stringifies the value it
receives using .toString() to convert a value of any type into a string representation. This is
the same as the usual behavior for getting and setting attributes, as with .getAttribute(...) and
.setAttribute(...).
'number'
The getter attempts to parse a floating-point number from the attribute string. The setter requires
that the value it receives is a Number, a BigInt, or a String from which a number can be
parsed.
'bigint'
The getter attempts to parse a BigInt from the attribute string. If the setter receives a
Number, it will first convert it to an integer before setting the attribute string. Otherwise, it
will use JS's built-in BigInt(...) conversion function
before stringifying the value.
'int'/'integer'
The getter returns an integer Number value as parsed from the attribute string. The setter
converts its received argument to a Number, and then rounds it.
'bool'/'boolean'
The getter returns false if the attribute string is the empty string, or true if it's non-empty.
The setter converts its received value to a Boolean using the standard Javascript truthiness
rules.
Enums
Enums are special types that limit their possible values to a particular set of Strings. To
declare an attribute as an enum type, set its type as an array of strings:
WebComponentAttributesMixin(MyWebComponent, {
classicalElement: {
type: ["fire", "earth", "water", "air"],
},
});An enum getter returns the default value if the attribute string doesn't match one of the elements
of its array exactly. An enum setter will throw an error if it receives a value that doesn't match.
'tokenlist'
Attributes of the tokenlist type are different from all other types, because instead of getting
and setting the property directly, it exposes a DOMTokenList which you can use to access the
attribute as a set of tokens. This works the same as the .classList property. Also unlike other
attributes, tokenlist attributes do not allow specifying a default value. If the backing HTML
attribute does not exist on the element, the tokenlist will act as if the attribute's value were the
empty string containing no tokens.
WebComponentAttributesMixin(MyWebComponent, {
myTokenlist: {
type: 'tokenlist',
},
});
// ...
const myComponent = new MyWebComponent();
myComponent.myTokenlist.add("red", "green", "blue");
// prints "red green blue"
console.log(myComponent.getAttribute('my-tokenlist'));