CustomElement

The core base class for building Monster custom elements with options, lifecycle hooks and templating support.

Import
the javascript logo
import { CustomElement } from "@schukai/monster/source/dom/customelement.mjs";
Source
the git logo
Package
the npm logo
Since
1.7.0

Introduction

CustomElement is the base class for Monster web components. It gives a custom element a consistent contract for options, templates, shadow root creation, stylesheet adoption and lifecycle wiring.

Core Responsibilities

SurfaceWhat it solves
getTag()Defines the tag name used for registration and template lookup.
defaultsProvides the stable option object for templates, labels, features and behaviour.
templates.main or document templateControls the initial DOM that will be cloned into the shadow root.
getCSSStyleSheet()Provides the adopted stylesheet or inline fallback styles for the component shell.
setOption() and setOptions()Updates the reactive option tree after the element has already been connected.

What Developers Usually Need First

  • Registration: implement getTag() and register the class once.
  • Template source: use inline templates.main for small components and document templates for shared markup.
  • Options: read declarative input from data-monster-options and runtime input from setOption().
  • Observers: attach an Observer when the visual shell must react to option changes.
  • Visibility: use show(), hide() and toggleVisibility() instead of manual DOM hacks.

Minimal Setup


import { CustomElement, registerCustomElement } from "@schukai/monster/source/dom/customelement.mjs";

class MyElement extends CustomElement {
  static getTag() {
    return "my-element";
  }

  get defaults() {
    return {
      ...super.defaults,
      templates: {
        main: "<p>Hello from CustomElement</p>",
      },
    };
  }
}

registerCustomElement(MyElement);

Options Flow

The option tree starts with defaults, then merges declarative input from data-monster-options or script references. After connection, use setOption(path, value) for single-path updates and setOptions(object) for larger merges.

Register A Minimal Element With Inline Template And Stylesheet

import {
    CustomElement,
    registerCustomElement,
} from "@schukai/monster/source/dom/customelement.mjs";

const tagName = "monster-doc-card";

const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(`
    :host {
        display: block;
    }

    article {
        display: grid;
        gap: var(--monster-space-2);
        padding: var(--monster-space-4);
        border: 1px solid var(--monster-color-border-primary-2);
        border-radius: var(--monster-border-radius);
        background: var(--monster-bg-color-primary-1);
        color: var(--monster-color-primary-1);
    }

    h3 {
        margin: 0;
    }

    p {
        margin: 0;
    }
`);

if (!customElements.get(tagName)) {
    class MonsterDocCard extends CustomElement {
        static getTag() {
            return tagName;
        }

        get defaults() {
            return {
                ...super.defaults,
                templates: {
                    main: `
                        <article part="card">
                            <h3>Monster Card</h3>
                            <p>
                                CustomElement can render from an inline template and adopt a local stylesheet.
                            </p>
                        </article>
                    `,
                },
            };
        }

        static getCSSStyleSheet() {
            return styleSheet;
        }
    }

    registerCustomElement(MonsterDocCard);
}
Open in playground

Read Declarative Options From Data Monster Options

import {
    CustomElement,
    registerCustomElement,
} from "@schukai/monster/source/dom/customelement.mjs";
import { Observer } from "@schukai/monster/source/types/observer.mjs";

const tagName = "monster-doc-status";

const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(`
    :host {
        display: block;
    }

    article {
        display: grid;
        gap: var(--monster-space-1);
        padding: var(--monster-space-4);
        border: 1px solid var(--monster-color-border-primary-2);
        border-radius: var(--monster-border-radius);
        background: var(--monster-bg-color-primary-1);
        color: var(--monster-color-primary-1);
    }

    [data-tone="live"] {
        background: var(--monster-bg-color-secondary-2);
        color: var(--monster-color-secondary-2);
        border-color: var(--monster-color-border-secondary-2);
    }

    p {
        margin: 0;
    }
`);

if (!customElements.get(tagName)) {
    class MonsterDocStatus extends CustomElement {
        static getTag() {
            return tagName;
        }

        get defaults() {
            return {
                ...super.defaults,
                headline: "Untitled",
                status: "idle",
                tone: "stable",
                templates: {
                    main: `
                        <article part="surface">
                            <strong part="headline"></strong>
                            <p part="status"></p>
                        </article>
                    `,
                },
            };
        }

        static getCSSStyleSheet() {
            return styleSheet;
        }

        connectedCallback() {
            super.connectedCallback();

            if (!this.renderObserver) {
                this.renderObserver = new Observer(() => this.render());
                this.attachObserver(this.renderObserver);
            }

            this.render();
        }

        render() {
            this.shadowRoot.querySelector("[part=headline]").textContent = this.getOption("headline", "Untitled");
            this.shadowRoot.querySelector("[part=status]").textContent = this.getOption("status", "idle");
            this.shadowRoot.querySelector("article").dataset.tone = this.getOption("tone", "stable");
        }
    }

    registerCustomElement(MonsterDocStatus);
}

const element = document.getElementById("custom-element-options-demo");

document.getElementById("custom-element-options-set-running").addEventListener("click", () => {
    element.setOptions({
        headline: "Documentation Build",
        status: "The build is running",
        tone: "stable",
    });
});

document.getElementById("custom-element-options-set-live").addEventListener("click", () => {
    element.setOptions({
        headline: "Release Live",
        status: "The documentation update is visible on production",
        tone: "live",
    });
});
Open in playground

Update Component State Via SetOption And SetOptions

import {
    CustomElement,
    registerCustomElement,
} from "@schukai/monster/source/dom/customelement.mjs";
import { Observer } from "@schukai/monster/source/types/observer.mjs";

const tagName = "monster-doc-counter";

const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(`
    :host {
        display: block;
    }

    article {
        display: flex;
        justify-content: space-between;
        align-items: center;
        gap: var(--monster-space-3);
        padding: var(--monster-space-4);
        border: 1px solid var(--monster-color-border-primary-2);
        border-radius: var(--monster-border-radius);
        background: var(--monster-bg-color-primary-1);
        color: var(--monster-color-primary-1);
    }

    article[data-tone="highlight"] {
        background: var(--monster-bg-color-secondary-2);
        color: var(--monster-color-secondary-2);
        border-color: var(--monster-color-border-secondary-2);
    }

    strong,
    span {
        display: block;
    }
`);

if (!customElements.get(tagName)) {
    class MonsterDocCounter extends CustomElement {
        static getTag() {
            return tagName;
        }

        get defaults() {
            return {
                ...super.defaults,
                counter: {
                    value: 0,
                    label: "Builds today",
                    tone: "default",
                },
                templates: {
                    main: `
                        <article>
                            <div>
                                <strong part="label"></strong>
                                <small part="hint">Updated through the option tree</small>
                            </div>
                            <span part="value"></span>
                        </article>
                    `,
                },
            };
        }

        static getCSSStyleSheet() {
            return styleSheet;
        }

        connectedCallback() {
            super.connectedCallback();

            if (!this.renderObserver) {
                this.renderObserver = new Observer(() => this.render());
                this.attachObserver(this.renderObserver);
            }

            this.render();
        }

        render() {
            this.shadowRoot.querySelector("[part=label]").textContent = this.getOption("counter.label", "Counter");
            this.shadowRoot.querySelector("[part=value]").textContent = String(this.getOption("counter.value", 0));
            this.shadowRoot.querySelector("article").dataset.tone = this.getOption("counter.tone", "default");
        }
    }

    registerCustomElement(MonsterDocCounter);
}

const element = document.getElementById("custom-element-counter-demo");

document.getElementById("custom-element-counter-increase").addEventListener("click", () => {
    element.setOption("counter.value", element.getOption("counter.value", 0) + 1);
});

document.getElementById("custom-element-counter-reset").addEventListener("click", () => {
    element.setOptions({
        counter: {
            value: 0,
            label: "Builds today",
            tone: "default",
        },
    });
});

document.getElementById("custom-element-counter-highlight").addEventListener("click", () => {
    element.setOptions({
        counter: {
            value: 8,
            label: "Builds published",
            tone: "highlight",
        },
    });
});
Open in playground

Resolve The Template From The Surrounding Document

import {
    CustomElement,
    registerCustomElement,
} from "@schukai/monster/source/dom/customelement.mjs";
import { Observer } from "@schukai/monster/source/types/observer.mjs";

const tagName = "monster-doc-template";

const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(`
    :host {
        display: block;
    }

    article {
        display: grid;
        gap: var(--monster-space-2);
        padding: var(--monster-space-4);
        border-inline-start: 0.35rem solid var(--monster-color-border-secondary-2);
        background: var(--monster-bg-color-secondary-2);
        color: var(--monster-color-secondary-2);
    }

    h3,
    p {
        margin: 0;
    }
`);

if (!customElements.get(tagName)) {
    class MonsterDocTemplate extends CustomElement {
        static getTag() {
            return tagName;
        }

        get defaults() {
            return {
                ...super.defaults,
                headline: "Template",
                body: "Resolved from document markup.",
            };
        }

        static getCSSStyleSheet() {
            return styleSheet;
        }

        connectedCallback() {
            super.connectedCallback();

            if (!this.renderObserver) {
                this.renderObserver = new Observer(() => this.render());
                this.attachObserver(this.renderObserver);
            }

            this.render();
        }

        render() {
            this.shadowRoot.querySelector("[part=headline]").textContent = this.getOption("headline", "Template");
            this.shadowRoot.querySelector("[part=body]").textContent = this.getOption("body", "Resolved from document markup.");
        }
    }

    registerCustomElement(MonsterDocTemplate);
}
Open in playground

React To Show Hide And Visibility Events

import {
    CustomElement,
    registerCustomElement,
} from "@schukai/monster/source/dom/customelement.mjs";

const tagName = "monster-doc-visibility";

const styleSheet = new CSSStyleSheet();
styleSheet.replaceSync(`
    :host {
        display: block;
    }

    article {
        padding: var(--monster-space-4);
        border: 1px solid var(--monster-color-border-primary-2);
        border-radius: var(--monster-border-radius);
        background: var(--monster-bg-color-primary-1);
        color: var(--monster-color-primary-1);
    }

    p {
        margin: 0;
    }
`);

if (!customElements.get(tagName)) {
    class MonsterDocVisibility extends CustomElement {
        static getTag() {
            return tagName;
        }

        get defaults() {
            return {
                ...super.defaults,
                templates: {
                    main: `
                        <article>
                            <p>This element uses the built in visibility helpers.</p>
                        </article>
                    `,
                },
            };
        }

        static getCSSStyleSheet() {
            return styleSheet;
        }
    }

    registerCustomElement(MonsterDocVisibility);
}

const element = document.getElementById("custom-element-visibility-demo");
const log = document.getElementById("custom-element-visibility-log");

const appendLog = (message) => {
    log.textContent = `${log.textContent}\n${message}`;
};

element.addEventListener("monster-visibility-changed", (event) => {
    appendLog(`visible: ${String(event.detail.visible)}`);
});

document.getElementById("custom-element-show").addEventListener("click", () => element.show());
document.getElementById("custom-element-hide").addEventListener("click", () => element.hide());
document.getElementById("custom-element-toggle").addEventListener("click", () => element.toggleVisibility());
Open in playground

Exported

CustomElement, initMethodSymbol, assembleMethodSymbol, attributeObserverSymbol, registerCustomElement, getSlottedElements, updaterTransformerMethodsSymbol

Derived from

HTMLElement

Options

The Options listed in this section are defined directly within the class. This class is derived from several parent classes. Therefore, it inherits Options from these parent classes. If you cannot find a specific Options in this list, we recommend consulting the documentation of the HTMLElement.

Option
Type
Default
Description
disabled
boolean
false
er the control is disabled. When present, it makes the element non-mutable, non-focusable, and non-submittable with the form.
shadowMode
string
open
de of the shadow root. When set to `open`, elements in the shadow root are accessible from JavaScript outside the root, while setting it to `closed` denies access to the root's nodes from JavaScript outside it.
delegatesFocus
boolean
true
or of the control with respect to focusability. When set to `true`, it mitigates custom element issues around focusability. When a non-focusable part of the shadow DOM is clicked, the first focusable part is given focus, and the shadow host is given any available :focus styling.
templates
object
undefined
Specifies the templates used by the control.
templates.main
string
undefined
te used by the control.
templateMapping
object
undefined
Specifies the mapping of templates.
templateFormatter
object
undefined
Specifies the formatter for the templates.
templateFormatter.marker
object
undefined
Specifies the marker for the templates.
templateFormatter.marker.open
function
null
e templates.
templateFormatter.marker.close
function
null
templates.
templateFormatter.i18n
boolean
false
es should be formatted with i18n.
eventProcessing
boolean
false
control processes events.
updater
object
undefined
Specifies updater options.
updater.batchUpdates
boolean
false
ribute updates per diff.

Properties

The Properties listed in this section are defined directly within the class. This class is derived from several parent classes. Therefore, it inherits Properties from these parent classes. If you cannot find a specific Properties in this list, we recommend consulting the documentation of the HTMLElement.

Methods

The methods listed in this section are defined directly within the class. This class is derived from several parent classes. Therefore, it inherits methods from these parent classes. If you cannot find a specific method in this list, we recommend consulting the documentation of the HTMLElement.

Constructor

constructor()
Throws
  • {Error} the option attribute does not contain a valid JSON definition.
A new object is created. First, the initOptions method is called. Here the options can be defined in derived classes. Subsequently, the shadowRoot is initialized. IMPORTANT: CustomControls instances are not created via the constructor, but either via a tag in the HTML or via document.createElement().

Behavioral methods

addAttributeObserver(attribute,callback)
Parameters
  • attribute: attribute
  • callback: callback
Returns
  • {CustomElement}
hide()
Hides the host element.
removeAttributeObserver(attribute)
Parameters
  • attribute: attribute
Returns
  • {CustomElement}
show()
Shows the host element.
toggleVisibility(force)
Parameters
  • force {boolean}: [force]
Toggles the host visibility.
updateI18n()
Returns
  • {CustomElement}
Throws
  • {Error} Cannot find an element with translations. Add a translation object to the document.
This method updates the labels of the element. The labels are defined in the option object. The key of the label is used to retrieve the translation from the document. If the translation is different from the label, the label is updated. Before you can use this method, you must have loaded the translations.

State query methods

hasNode(node)1.19.0
Parameters
  • node {node}: - The node to check for within this component's child nodes.
Returns
  • {boolean}: true if the given node is found, otherwise false.
Throws
  • {TypeError} value is not an instance of
Checks if the provided node is part of this component's child nodes, including those within the shadow root, if present.
isVisible()
Alias for the current host visibility state.

Structural methods

getInternalUpdateCloneData()
Returns
  • {*}
You know what you are doing? This function is only for advanced users. The result is a clone of the internal data.
getOption(path,)1.10.0
Parameters
  • path {string}: path
  • undefined {*}: defaultValue
Returns
  • {*}
nested options can be specified by path a.b.c
setOption(path,value)1.14.0
Parameters
  • path {string}: path
  • value {*}: value
Returns
  • {CustomElement}
Set option and inform elements
setOptions(options)1.15.0
Parameters
  • options {string|object}: options
Returns
  • {CustomElement}
setVisible(visible)
Parameters
  • visible {boolean}: visible
Sets the host visibility.

Static methods

[instanceSymbol]()2.1.0
Returns
  • {symbol}
This method is called by the instanceof operator.
getCSSStyleSheet()
Returns
  • {cssstylesheet|cssstylesheet[]|string|undefined}: `CSSStyleSheet` object or an array of such objects that define the styles for the custom element, or `undefined` if no stylesheet should be applied.
The getCSSStyleSheet() method returns a CSSStyleSheet object that defines the styles for the custom element. If the environment does not support the CSSStyleSheet constructor, then an object can be built using the provided detour. If undefined is returned, then the shadow root does not receive a stylesheet. Example usage:
class MyElement extends CustomElement {
  static getCSSStyleSheet() {
    const sheet = new CSSStyleSheet();
    sheet.replaceSync("p { color: red; }");
    return sheet;
  }
}
If the environment does not support the CSSStyleSheet constructor, you can use the following workaround to create the stylesheet:
const doc = document.implementation.createHTMLDocument("title");
let style = doc.createElement("style");
style.innerHTML = "p { color: red; }";
style.appendChild(document.createTextNode(""));
doc.head.appendChild(style);
return doc.styleSheets[0];
getTag()1.7.0
Returns
  • {string}: tag name associated with the custom element.
Throws
  • {Error} This method must be overridden by the derived class.
The getTag() method returns the tag name associated with the custom element. This method should be overwritten by the derived class. Note that there is no check on the name of the tag in this class. It is the responsibility of the developer to assign an appropriate tag name. If the name is not valid, the registerCustomElement() method will issue an error.
observedAttributes()1.15.0
Returns
  • {string[]}
This method determines which attributes are to be monitored by attributeChangedCallback(). Unfortunately, this method is static. Therefore, the observedAttributes property cannot be changed during runtime.

Lifecycle methods

Lifecycle methods are called by the environment and are usually not intended to be called directly.

adoptedCallback()1.7.0
Returns
  • {void}
The custom element has been moved into a new document (e.g. someone called document.adoptNode(el)).
attributeChangedCallback(attrName,oldVal,newVal)1.15.0
Parameters
  • attrName {string}: attrName
  • oldVal {string}: oldVal
  • newVal {string}: newVal
Returns
  • {void}
Called when an observed attribute has been added, removed, updated, or replaced. Also called for initial values when an element is created by the parser, or upgraded. Note: only attributes listed in the observedAttributes property will receive this callback.
connectedCallback()1.7.0
Returns
  • {void}
This method is called every time the element is inserted into the DOM. It checks if the custom element has already been initialized and if not, calls the assembleMethod to initialize it.
disconnectedCallback()1.7.0
Returns
  • {void}
Called every time the element is removed from the DOM. Useful for running clean up code.

Other methods

[assembleMethodSymbol]()1.8.0
Returns
  • {customelement}: The updated custom element.
This method is called once when the object is included in the DOM for the first time. It performs the following actions:
  1. Extracts the options from the attributes and the script tag of the element and sets them.
  2. Initializes the shadow root and its CSS stylesheet (if specified).
  3. Initializes the HTML content of the element.
  4. Initializes the custom elements inside the shadow root and the slotted elements.
  5. Attaches a mutation observer to observe changes to the attributes of the element.
[initMethodSymbol]()1.8.0
Returns
  • {CustomElement}
Is called once via the constructor
[updaterTransformerMethodsSymbol]()2.43.0
Returns
  • {object}
This method is called once when the object is equipped with update for the dynamic change of the dom. The functions returned here can be used as pipe functions in the template. In the example, the function my-transformer is defined. In the template, you can use it as follows:
<my-element
  data-monster-option-transformer="path:my-value | call:my-transformer"
>
</my-element>
The function my-transformer is called with the value of my-value as a parameter.
class MyElement extends CustomElement {
  [updaterTransformerMethodsSymbol]() {
    return {
      "my-transformer": (value) => {
        switch (typeof Wert) {
          case "string":
            return value + "!";
          case "Zahl":
            return value + 1;
          default:
            return value;
        }
      },
    };
  }
}
attachObserver(observer)
Parameters
  • observer {observer}: observer
Returns
  • {CustomElement}
attach a new observer
callCallback(name,args)
Parameters
  • name {string}: - The name of the callback to be executed.
  • args {array}: - An array of arguments to be passed to the callback function.
Returns
  • {*}: result of the callback function execution.
Invokes a callback function with the given name and arguments.
containsObserver(observer)
Parameters
  • observer {observer}: observer
Returns
  • {ProxyObserver}
customization()
The customization property allows overwriting the defaults. Unlike the defaults that expect an object, the customization is a Map. This also allows overwriting individual values in a deeper structure without having to redefine the entire structure and thus changing the defaults.
detachObserver(observer)
Parameters
  • observer {observer}: observer
Returns
  • {CustomElement}
detach a observer
visible()
Returns whether the host element is currently visible.

Events

This component does not fire any public events. It may fire events that are inherited from its parent classes.

The current width of the area is too small to display the content correctly.