CustomElement
The core base class for building Monster custom elements with options, lifecycle hooks and templating support.
import { CustomElement } from "@schukai/monster/source/dom/customelement.mjs";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
| Surface | What it solves |
|---|---|
getTag() | Defines the tag name used for registration and template lookup. |
defaults | Provides the stable option object for templates, labels, features and behaviour. |
templates.main or document template | Controls 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.mainfor small components and document templates for shared markup. - Options: read declarative input from
data-monster-optionsand runtime input fromsetOption(). - Observers: attach an
Observerwhen the visual shell must react to option changes. - Visibility: use
show(),hide()andtoggleVisibility()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);
}Monster Card
CustomElement can render from an inline template and adopt a local stylesheet.
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",
});
});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",
},
});
});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);
}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());ready
This element uses the built in visibility helpers.
Exported
CustomElement, initMethodSymbol, assembleMethodSymbol, attributeObserverSymbol, registerCustomElement, getSlottedElements, updaterTransformerMethodsSymbolDerived from
HTMLElementOptions
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.
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(){Error}the option attribute does not contain a valid JSON definition.
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)attribute: attributecallback: callback
- {CustomElement}
hide()removeAttributeObserver(attribute)attribute: attribute
- {CustomElement}
show()toggleVisibility(force)force{boolean}: [force]
updateI18n()- {CustomElement}
{Error}Cannot find an element with translations. Add a translation object to the document.
State query methods
hasNode(node)1.19.0node{node}: - The node to check for within this component's child nodes.
- {boolean}: true if the given node is found, otherwise false.
{TypeError}value is not an instance of
isVisible()Structural methods
getInternalUpdateCloneData()- {*}
getOption(path,)1.10.0path{string}: pathundefined{*}: defaultValue
- {*}
a.b.csetOption(path,value)1.14.0path{string}: pathvalue{*}: value
- {CustomElement}
setOptions(options)1.15.0options{string|object}: options
- {CustomElement}
setVisible(visible)visible{boolean}: visible
Static methods
[instanceSymbol]()2.1.0- {symbol}
instanceof operator.getCSSStyleSheet()- {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.
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- {string}: tag name associated with the custom element.
{Error}This method must be overridden by the derived class.
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- {string[]}
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- {void}
attributeChangedCallback(attrName,oldVal,newVal)1.15.0attrName{string}: attrNameoldVal{string}: oldValnewVal{string}: newVal
- {void}
connectedCallback()1.7.0- {void}
disconnectedCallback()1.7.0- {void}
Other methods
[assembleMethodSymbol]()1.8.0- {customelement}: The updated custom element.
- Extracts the options from the attributes and the script tag of the element and sets them.
- Initializes the shadow root and its CSS stylesheet (if specified).
- Initializes the HTML content of the element.
- Initializes the custom elements inside the shadow root and the slotted elements.
- Attaches a mutation observer to observe changes to the attributes of the element.
[initMethodSymbol]()1.8.0- {CustomElement}
[updaterTransformerMethodsSymbol]()2.43.0- {object}
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)observer{observer}: observer
- {CustomElement}
callCallback(name,args)name{string}: - The name of the callback to be executed.args{array}: - An array of arguments to be passed to the callback function.
- {*}: result of the callback function execution.
containsObserver(observer)observer{observer}: observer
- {ProxyObserver}
customization()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)observer{observer}: observer
- {CustomElement}
visible()Events
This component does not fire any public events. It may fire events that are inherited from its parent classes.