html-element-mixins v0.3.1
# html-element-mixins
A collection of mixins enhancing HTMLElement
, powering CustomElements.
Installation
$ npm install html-element-mixins
Introduction
Set menu or à la carte?
Custom Elements are a powerful part of the Web Components standard. As its built-in functionality is limited, web developers need a little sugar to make life easier.
Custom base classes like LitElement offer a great deal of convenience, but they introduce a new problem.
Using a custom base class is like ordering a set menu in a restaurant: The cook (code author) serves a fixed amount of dishes (functionality) for a fixed price (bundle size, execution time). Set menus are convenient, and the food can be really tasty. However, one may eat too much or dislike some of the dishes.
html-element-mixins
is like ordering à la carte. It's a collection of mixins (dishes) that can be added to HTMLElement
. As a developer, it's up to you which ones to add (order). As a result, one gets exactly the functionality (dishes) they need,
therefore allowing you to build your own base class (eat à la carte!), eating only the dishes you love (minimal overhead, bundle size & execution time).
Collection
html-element-mixins
is a collection of mixins, extending the power of the HTMLElement
class. In short, this is what they are about:
- ObservedProperties enables observed properties (just like built-in
observedAttributes
). - DOMProperties enables attribute to property synchonisation.
- ReflectedProperties enables property to attribute synchonisation.
Furthermore, we created a bunch of addons:
- PropertiesChangedCallback Debounces / batches property changes for efficient DOM-rendering.
- PropertyChangedHandler enables change handlers methods on property changes.
- PropertiesChangedHandler adds change handlers methods for multiple property changes.
- PropertyChangedEvent dispatches events upon property changes.
- Property wraps all property mixins a single configuration.
- EnsuredAttributes adds default values for (non-observing) attributes.
- KeyBindings adds default values for (non-observing) attributes.
ObservedProperties
import { ObservedProperties } from 'html-element-mixins';
Observing
By default, Custom Elements can observe attribute value changes whitelisted in the observedAttributes
Array. ObservedProperties
offers a similar solution for DOM properties using observedProperties
.
When a property has changed, propertyChangedCallback
is called, passing the property name, the old value and the new value.'
class DemoElement extends ObservedProperties(HTMLElement) {
static get observedProperties() {
return ['firstName', 'lastName', 'age']
}
propertyChangedCallback(propName, oldValue, newValue) {
console.info(`${propName} changed from ${oldValue} to ${newValue}`);
}
}
If you like you can add your own getter / setter pairs:
static get observedProperties() {
return ['initials']
}
get initials() {
return this._initials;
}
set initials(val) {
this._initials = val.toUpperCase();
}
constructor() {
this.initials = 'a.b.c.';
}
propertyChangedCallback(propName, oldValue, newValue) {
console.info(`${propName} changed to ${newValue}`); //initials changed to A.B.C;
}
Accessors don't require a getter / setter pair. Keep in mind though that by default, private property values are assigned using the following pattern: #${propName}
.
static get observedProperties() {
return ['firstName']
}
get firstName() {
return this['#firstName'].toLowerCase()
}
DOMProperties
Some native properties (e.g. input value
) can be set using a DOM attribute. This mixin adds exactly this behavior: attribute to property sync:
import { DOMProperties } from 'html-element-mixins';
class DemoElement extends DOMProperties(HTMLElement) {
static get DOMProperties() {
return ['firstname', 'lastname']
}
}
<demo-element
id="demo"
firstname="Adewale"
lastname="King"
></demo-element>
<script>
console.info(demo.firstname, demo.lastname); // Adewale, King
</script>
By default, attributes are lowercased property names (e.g. 'myPropName' becomes 'mypropname'). You can configure custom attribute mappings using 'propertyAttributeNames':
static get DOMProperties() {
return ['myBestFriend']
}
static get propertyAttributeNames() {
return {
myBestFriend: 'my-best-friend',
}
}
<demo-element
id="demo"
my-best-friend="Hellen"
></demo-element>
Attribute Getters
Attribute values are always strings. For boolean, numeric or JSON like property values, this may result in property values. Using the 'propertyAttributeGetters' config, one can add getter functions for attributes when properties are set:
static get reflectedProperties() {
return ['married', 'friends']
}
static get propertyAttributeGetters() {
return {
married: function(value) {
if(value === '') return true;
return false;
},
friends: function(value) {
if(!value) return null;
return JSON.parse(value);
}
}
}
<demo-element
id="demo"
married
friends='["Gabriella","Anik","Linda"]'
></demo-element>
<script>
console.info(demo.married, demo.friends); //true, ['Gabriella','Anik','Linda'];
</script>
html-element-mixin
comes with a set of attribute getters for Boolean
, String
, Number
and JSON
compatible types:
import { ParseString, ParseNumber, ParseBoolean, ParseJSON } from 'html-element-mixins/utils/attribute-accessors';
static get propertyAttributeGetters() {
return {
firstName: ParseString.fromAttribute,
age: ParseNumber.fromAttribute,
married: ParseBoolean.fromAttribute,
friends: ParseJSON.fromAttribute
}
}
ReflectedProperties
import { ReflectedProperties, ObservedProperties } from 'html-element-mixins';
This enables property to attribute sync. Using the 'reflectedProperties' object, one can map properties (keys) to attributes (values). The ObservedProperties mixin is required.
class DemoElement extends ReflectedProperties(ObservedProperties(HTMLElement)) {
static get observedProperties() {
return ['firstname', 'lastname', 'age']
}
static get reflectedProperties() {
return ['firstname', 'lastname', 'age']
}
constructor() {
this.firstname = 'Amira';
this.firstname = 'Arif';
this.age = 24;
}
}
By default, attributes are lowercased property names (e.g. 'myPropName' becomes 'mypropname'). You can configure custom attribute mappings using 'propertyAttributeNames':
static get reflectedProperties() {
return ['firstName']
}
static get propertyAttributeNames() {
return {
firstName: 'first-name',
}
}
<demo-element first-name="Amira"></demo-element>
Attribute Setters
Attribute values are always strings. For boolean, numeric or JSON like property values, this may result in undesired attribute reflections. Using the 'propertyAttributeSetters' config, one can add setter functions for reflected properties. Attributes are set based on the return value of these functions: when false
or undefined
, the removeAttribute
is called. Otherwise, setAttribute
is called using the return value:
static get reflectedProperties() {
return ['married', 'friends']
}
static get propertyAttributeSetters() {
return {
married: function(value) {
return value === true;
},
friends: function(value) {
if(!value) return;
return JSON.stringify(value);
}
}
}
constructor() {
this.married = false;
this.friends = ['Gabriella', 'Anik', 'Linda'];
}
<demo-element
married
friends='["Gabriella","Anik","Linda"]'
></demo-element>
html-element-mixins come with a set of attribute parsers for Boolean
, String
, Number
and JSON
compatible types:
import { ParseString, ParseNumber, ParseBoolean, ParseJSON } from 'html-element-mixins/utils/attribute-accessors';
static get propertyAttributeSetters() {
return {
firstName: ParseString.toAttribute,
age: ParseNumber.toAttribute,
married: ParseBoolean.toAttribute,
friends: ParseJSON.toAttribute
}
}
PropertiesChangedCallback
import { PropertiesChangedCallback, ObservedProperties } from 'html-element-mixins';
When declaring observed properties using the observedProperties
array, property changes are fired each time a a property changes using the propertyChangedCallback
. For efficiency reasons (e.g. when rendering DOM), the propertiesChangedCallback
(plural!) can be used. This callback is debounced by cancel / requestAnimationFrame on every property change. In the following example, render
is invoked only once:
import { PropertiesChangedCallback } from 'html-element-mixins/src/properties/addons';
import { ObservedProperties } from 'html-element-mixins';
class DemoElement extends PropertiesChangedCallback(ObservedProperties(HTMLElement)) {
constructor() {
super();
this._renderCount = 0;
}
static get observedProperties() {
return ['firstName', 'lastName', 'age'];
}
propertiesChangedCallback(propNames, oldValues, newValues) {
this._renderCount++;
this.render();
}
render() {
this.innerHTML = `
Hello, ${this.firstName} ${this.lastName} (${this.age} years).<br>
Render Count = ${this._renderCount}.
`
}
constructor() {
super();
this.firstName = 'Amina';
this.lastName = 'Hamzaoui';
this.age = 24;
}
}
PropertyChangedHandler
import { ObservedProperties } from 'html-element-mixins';
import { PropertyChangedHandler } from 'html-element-mixins/src/properties/addons';
Value changes to properties whitelisted in the observedProperties
array are always notified using propertyChangedCallback
. PropertyChangedHandler provides for custom callbacks for property changes:
class DemoElement extends PropertyChangedHandler(ObservedProperties((HTMLElement)) {
static get observedProperties() {
return ['firstName']
}
static get propertyChangedHandlers() {
return {
firstName: function(newValue, oldValue) {
console.info('firstName changed!', newValue, oldValue);
}
}
}
}
Alternatively, callbacks can be passed as string references:
static get propertyChangedHandlers() {
return { firstName: '_firstNameChanged' }
}
_firstNameChanged(newValue, oldValue) {
console.info('firstName changed!', newValue, oldValue);
}
Note:
PropertyChangedHandler
should always be used in conjunction withObservedProperties
.
PropertiesChangedHandler
import { ObservedProperties } from 'html-element-mixins';
import { PropertiesChangedHandler } from 'html-element-mixins/src/properties/addons';
Its plural companion propertiesChangedHandlers
can be used to invoke a function when one of many properties have changed. Key / value pairs are now swapped. A key refers to the handler function, the value holds an array of the observed properties.
class DemoElement extends PropertiesChangedHandler(ObservedProperties((HTMLElement)) {
static get observedProperties() {
return ['firstName', 'lastName']
}
static get propertiesChangedHandlers() {
return {
_nameChanged: ['firstName', 'lastName']
}
}
_nameChanged(propNames, newValues, oldValues) {
console.info(newValues.firstName, newValues.lastName);
}
}
Note:
PropertiesChangedHandler
should always be used in conjunction withObservedProperties
.
PropertyChangedEvent
import { ObservedProperties } from 'html-element-mixins';
import { PropertiesChangedEvent } from 'html-element-mixins/src/properties/addons';
This mixin dispatches Events for changed properties whitelisted in the propertiesChangedEventNames
object.
class DemoElement extends PropertyChangedEventDispatcher(ObservedProperties((HTMLElement)) {
static get observedProperties() {
return ['firstName', 'age']
}
static get propertyChangedEventNames() {
return {
firstName: 'first-name-changed',
}
}
constructor() {
super();
this.firstName = 'John';
}
}
<demo-element id="demo"></demo-element>
<script>
demo.addEventListener('first-name-changed', (e => {
console.info(e.detail); //{value: 'John'}
})
</script>
You can also configure your own event, using propertyChangedEvent
. This enables you to fully control the event params:
class DemoElement extends PropertyChangedEventDispatcher(ObservedProperties((HTMLElement)) {
static get observedProperties() {
return ['firstName', 'age']
}
static get propertiesChangedEventNames() {
return {
firstName: 'first-name-changed',
ageChanged: 'age-changed'
}
}
static get propertyChangedEvent() {
return (eventName, propName, oldValue, newValue) => {
return new CustomEvent(eventName, {
detail: {eventName, propName, oldValue, newValue},
bubbles: true,
composed: true
});
};
}
constructor() {
super();
this.firstName = 'John';
}
}
<demo-element id="demo"></demo-element>
<script>
demo.addEventListener('first-name-changed', (e => {
console.info(e.detail); //{propName: 'age', oldValue: undefined, newValue: 'John'}
})
</script>
Note:
PropertyChangedEventDispatcher
should always be used in conjunction withObservedProperties
.
Property
import { Property } from 'html-element-mixins';
This wraps all property mixins into a single properties
configuration object.
class DemoElement extends Property(HTMLElement) {
static get properties() {
return {
firstName: {
observe: true, //add to `observedProperties` array
DOM: true, //add to `DOMProperties` array
reflect: true, //add to `reflectedProperties` array
attributeName: 'first-name', //custom attribute map
}
}
}
}
Attribute accessors can be added using attributeGetter
and attributeSetter
:
static get properties() {
return {
firstName: {
observe: true,
DOM: true,
reflect: true,
attributeSetter: ParseString.toAttribute,
attributeGetter: ParseString.fromAttribute
}
}
}
You may very well use the propertyChangedHandler
and propertyChandedEvent
addons:
import { PropertyChangedHandler, PropertyChangedEvent } from 'html-element-mixins/properties/addons';
static get properties() {
return {
firstName: {
observe: true,
DOM: true,
reflect: true,
changedHandler: function(oldValue, newValue) { console.info(`firstname changed to ${newValue}`); },
changedEventName: 'first-name-changed'
},
}
}
<demo-element id="demo"></demo-element>
<script>
demo.addEventListener('last-name-changed', (e) => {
console.info('last name changed!', e.detail);
})
</script>
DefaultAttributes
import { DefaultAttributes } from 'html-element-mixins/src/misc';
This ensures attributes are added to an element when added to a DOM tree, if no value is set. Attribute key/value pairs are configured via defaultAttributes
:
class DemoElement extends DefaultAttributes(HTMLElement) {
static get defaultAttributes() {
return {
tabIndex: 0,
role: 'button',
active: ''
}
}
}
<demo-element tabindex="0" role="button" active></demo-element>
Keep in mind that these attributes are only set once (after connectedCallback
) and should only be used for non-observing attributes.
KeyBindings
import { KeyBindings } from 'html-element-mixins/src/misc';
class DemoElement extends KeyBindings(HTMLElement) {
static get keyDownBindings() {
return {
'save': [
{key: 's', metaKey: true},
{key: 's', ctrlKey: true}
],
};
}
static get keyPressBindings() {
return {
'close': [{code: 'Escape'},
],
};
}
static get keyUpBindings() {
return {
'delete': [
{code: 'Backspace'}
]
};
}
save(e) {
e.preventDefault();
alert('Save!');
}
close() {
alert('Close!');
}
delete() {
alert('delete!')
}
}