Element Behaviors
Element Behaviors is a JavaScript library that allows you to define and apply reusable behaviors to HTML elements. It follows the Custom Elements specifications closely, providing a way to encapsulate and compose functionality onto elements without the restrictions of Custom Elements.
Introduction
Element Behaviors allows you to enhance HTML elements with reusable behaviors. These behaviors can be defined, applied, and managed using the Element Behaviors library.
Getting Started
To use Element Behaviors, include the library in your project. You can download it from GitHub or include it via a CDN script tag:
<!-- For the latest release. -->
<script src="https://cdn.jsdelivr.net/gh/caboodle-tech/element-behaviors@main/dist/eb.min.js"></script>
<!-- For older releases that have been tagged. Replace [TAG] with the proper semver number. -->
<script src="https://cdn.jsdelivr.net/gh/caboodle-tech/element-behaviors@[TAG]/dist/eb.min.js"></script>
Defining Behaviors
Behaviors are defined as JavaScript classes. Here is an example of a behavior that counts how many times it has been clicked:
This example could be simplified by removing the observedAttributes
feature. This allows the counter to be updated by modifying the count
attribute in addition to clicking on the element. See the Responding to Attribute Changes section for more information.
class ClickCounter {
static observedAttributes = ['count'];
#count;
#element;
#listener;
constructor(element) {
this.#element = element;
this.#listener = this.incrementCount.bind(this);
if(element.hasAttribute('count')) {
const count = Number(element.getAttribute('count')) || Number(0);
this.#count = count;
return;
}
element.setAttribute('count', 0);
this.#count = 0;
}
connectedCallback() {
this.render();
this.#element.addEventListener('click', this.#listener);
}
incrementCount() {
const count = Number(this.#element.getAttribute('count')) || Number(this.#count);
this.#element.setAttribute('count', count + 1);
}
disconnectedCallback() {
this.#element.removeEventListener('click', this.#listener);
}
render() {
this.#element.textContent = `Count: ${this.#count}`;
}
attributeChangedCallback(name, oldValue, newValue) {
if (name !== 'count') {
return;
}
this.#count = Number(newValue) || Number(oldValue) || 0;
this.render();
}
}
elementBehaviors.define('click-counter', ClickCounter);
Using Behaviors
To use a defined behavior, register it with Element Behaviors:
elementBehaviors.define('click-counter', ClickCounter);
Then, apply the behavior to an element by adding a has
attribute:
<div has="click-counter"></div>
Unlike Custom Elements, multiple behaviors can be added to a single element by separating them with spaces, you may also use single word behaviors names:
<div has="click-counter logger"></div>
Lifecycle Callbacks
Once your behavior is registered, the browser will call certain methods of your class when code in the page interacts with your behavior in certain ways. By providing an implementation of these methods, known as lifecycle callbacks, you can run code in response to these events.
-
connectedCallback()
: Called when the element is attached to the DOM. -
disconnectedCallback()
: Called when the element is detached from the DOM. -
adoptedCallback()
: Called when the element is moved to a new document. -
attributeChangedCallback(name: string, oldValue: string, newValue: string)
: Called when one of the observed attributes is changed. See the Responding to Attribute Changes section for more details about this callback.
Here is a minimal Behavior that logs these lifecycle events:
// Create a class for the element
class ExampleLifecycleBehavior {
static observedAttributes = ["color", "size"];
constructor(element) {
// Keep a reference to the element this instance belongs to
this.#element = element;
}
connectedCallback() {
console.log("Custom element added to page.");
}
disconnectedCallback() {
console.log("Custom element removed from page.");
}
adoptedCallback() {
console.log("Custom element moved to new page.");
}
attributeChangedCallback(name, oldValue, newValue) {
console.log(`Attribute ${name} has changed.`);
}
}
elementBehaviors.define("lifecycle-example", ExampleLifecycleBehavior);
You can test this behavior by adding the following code to your HTML and then interacting with it using your browser's developer tools. Make sure to open the console to see the log messages:
<div has="lifecycle-example"></div>
Responding to Attribute Changes
Like built-in elements, elements with behaviors can use HTML attributes to configure how the element behaves. To use attributes effectively, an element has to be able to respond to changes in an attribute's value. This is done by adding the following members to the class that implements the behavior:
-
A static property named
observedAttributes
. This must be an array containing the names of all attributes for which the element needs change notifications. -
An implementation of the
attributeChangedCallback()
lifecycle callback.
The attributeChangedCallback()
callback is then called whenever an attribute whose name is listed in the element's observedAttributes
property is added, modified, removed, or replaced.
API Reference
elementBehaviors.define(name, behaviorClass)
Registers a new behavior class.
-
name (string)
: The name of the behavior. -
behaviorClass (Class)
: The behavior class.
elementBehaviors.getBehaviorElements([limitToBehavior])
Returns an array of elements that have at least one behavior attached to them. You can limit this result to a specific behavior or subset of behaviors.
-
limitToBehavior (string, optional)
: A behavior name or behavior names separated by spaces to limit the results to. If not provided, all elements with behaviors are returned.
elementBehaviors.removeBehavior(element, behavior)
Removes a behavior including its state from an element.
-
element (HTMLElement|Array<HTMLElement>)
: An element or array of elements to remove the specified behaviors from. -
behavior (string)
: A behavior or behaviors separated by spaces to remove from the specified element or elements.
elementBehaviors.setObserverTimeout([ms])
Set a different restart interval for Element Behaviors observer.
-
ms (int)
: How often observer restarts should be allowed.
elementBehaviors.undefine(name, behaviorClass)
Completely remove a behavior from existence; automatically calls removeBehavior
on every element this behavior is found on.
-
name (string)
: The behavior you would like to remove from existence. -
behaviorClass (Class)
: The original class for this behavior or a reference to the class.
elementBehaviors.version()
Returns the version of Element Behaviors in use.
Limitations and Modified Behavior
Because Element Behaviors is not a native web standard there are some limitations and modified behaviors to be aware of:
-
Unlike other Element Behaviors libraries, behavior states are saved! Removing a behavior from an elements
has
attribute does not delete the state, allowing it to be preserved between document removals or document adoptions. If you no longer want a behaviors state permanently on an element, you must use theremoveBehavior
method to remove that behavior from an element. -
Attaches behaviors to elements by using a non-standard (unhyphenated)
has
attribute on the element. - Unhyphenated behavior names are supported in contrast to the Custom Elements standard of requiring hyphenated custom element names.
- Closed shadow DOMs are not supported.
-
Shadow DOMs are supported by monkey patching the
Element.prototype.attachShadow
method. - As expected, behavior elements are subject to the cross-origin security policy of iframes.
- Iframes not blocked by CORs policies are tracked by the parent document, but a script must be injected into the iframe to enable iframe shadow DOM support.
-
For efficiency, Element Behaviors uses a single
MutationObserver
to watch the document, shadow DOMs, and iframes. -
Element Behaviors must restart it's observer every time a shadow DOM or iframe behavior is created. To improve efficiency these behaviors are processed in batches every 50 milliseconds by default. Use the
elementBehaviors.setObserverTimeout
method to reduce or increase this interval.
Examples
The following examples demonstrate how to use Element Behaviors in your HTML. You can interact with the elements to see the behaviors in action. The JavaScript source code for each example is on GitHub.
A Simple Counter
Using the ClickCounter
behavior from the Defining Behaviors section, you can create a button that keeps track of how many times it has been clicked by adding the following code to your HTML:
<button has="click-counter"></button>
The button will then render on the page like this:
Observed Attributes and Lifecycle Events
The following div
is being used to demonstrate a modified version of the ExampleLifecycleBehavior
from the Lifecycle Callbacks section. You can use the buttons below to change the color
and size
attributes of the div
to see the attributeChangedCallback
event in action. Open the console to see the lifecycle events as you interact with the div
:
Shadow DOM
In this example we are again using the ClickCounter
behavior from the Defining Behaviors section. This time however, we are adding the button
inside an open
shadow DOM. You can interact with the button below just like the previous example:
Iframe, DOM Adoption, and States
The following example demonstrates how to use Element Behaviors in an iframe; the iframe is same-origin meaning CORs is not an issue for this demo. The button
is another ClickCounter
behavior. Notice that we are passing the button
from the iframe to this document and back every 5 seconds. This demonstrates not only adoptedCallback
in action, but also the preservation of the button
's state between documents:
Loader
The following example is a working demonstration of the Loader
behavior from the GitHub README. It uses the loader
attribute to set which loader to display. The visible
attribute is used to toggle the loader on and off: