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.

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:

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.

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.

elementBehaviors.removeBehavior(element, behavior)

Removes a behavior including its state from an element.

elementBehaviors.setObserverTimeout([ms])

Set a different restart interval for Element Behaviors observer.

elementBehaviors.undefine(name, behaviorClass)

Completely remove a behavior from existence; automatically calls removeBehavior on every element this behavior is found on.

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:

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:

Document
Iframe

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:



The page is loading...