CustomElement
CustomElement is the base layer for Monster web components. Use it when you need your own tag, your own template and Shadow DOM encapsulation, but not yet the full form-associated behavior of CustomControl.
Build a minimal reusable element and understand the lifecycle you are opting into
This tutorial focuses on the smallest useful custom element: a custom tag with defaults, template output, registration and a clear separation between one-time init and DOM assembly.
Before You Start
Be comfortable with loading Monster at all
This page assumes the install and file layout are already clear. If not, start with First Page before you build a custom tag.
Know when Updater is enough
If you only need to bind an existing DOM subtree to data, use Templating. Reach for CustomElement when you truly need your own reusable tag and lifecycle.
Choose the Right Base Class
Use CustomElement when
You need a standalone tag, Shadow DOM, template composition and public options, but the element is not primarily a form control.
Use CustomControl when
You need value handling, form association, validation and ElementInternals-backed behavior. That is the right layer for select-like and input-like controls.
What the Base Class Gives You
Shadow DOM management
The base class owns the Shadow DOM setup. That is why lifecycle overrides should call super unless you know exactly which internal behavior you are replacing.
Template and option infrastructure
Defaults, template mapping and formatter markers already exist. Extend them instead of inventing parallel configuration layers.
Build a Minimal Element
1. Import the base class and registration helper
import {
CustomElement,
registerCustomElement,
} from "@schukai/monster/source/dom/customelement.mjs";2. Create the class and define the tag
class MyButton extends CustomElement {
static getTag() {
return "monster-my-button";
}
}3. Extend defaults instead of replacing them
Keep inherited behavior intact. Add only the options and template pieces you actually need.
class MyButton extends CustomElement {
static getTag() {
return "monster-my-button";
}
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: "<button>${label}</button>",
},
templateMapping: {
label: "Hello!",
},
});
}
}4. Register the element once
registerCustomElement(MyButton);5. Use the tag in HTML
<monster-my-button></monster-my-button>Result
The point of this example is not the button styling. It is the contract: explicit tag, explicit registration and explicit template ownership.
Template Mapping and Formatter Markers
Use templateMapping when a template should stay static but receive values from options. Only customize formatter markers if you truly need a different syntax boundary.
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: "<button><%label%></button>",
},
templateMapping: {
label: "Mapped label",
},
templateFormatter: {
marker: {
open: "<%",
close: "%>",
},
},
});
}Do not over-customize the template layer early
If every local control uses its own formatter syntax or mapping scheme, the library stops feeling like a system. Keep the defaults unless you have a strong reason.
Lifecycle Hooks You Actually Need
connectedCallback()
Use it for runtime work that depends on the element being in the DOM. Always call super.connectedCallback() so the base class can finish setup.
disconnectedCallback()
Use it to clean up listeners, timers or observers that you added yourself. Again, keep the parent call intact.
class MyButton extends CustomElement {
connectedCallback() {
super.connectedCallback();
// add listeners or runtime setup here
}
disconnectedCallback() {
super.disconnectedCallback();
// clean up your own resources here
}
}Init vs Assemble
Monster splits one-time object initialization from DOM assembly through initMethodSymbol and assembleMethodSymbol. In Monster 5 this boundary also defines when the options API is available. Existing Monster 4 subclasses should follow the complete Monster 5 migration guide before upgrading.
defaults and customization declare initial state
Keep both getters pure. Do not call getOption(), setOption() or setOptions() there, including through a helper. The option observer does not exist yet and Monster throws OptionsNotInitializedError.
initMethodSymbol is the first option-safe hook
The observer exists before this hook runs. Use it for one-time state derived from initialized options, but not for work that requires connected DOM or script-based configuration.
assembleMethodSymbol handles DOM-dependent work
Use assembly for option-dependent markup and for values supplied after construction, including script configuration. Call the parent hook before relying on base assembly.
import {
CustomElement,
initMethodSymbol,
assembleMethodSymbol,
} from "@schukai/monster/source/dom/customelement.mjs";
class MyButton extends CustomElement {
get defaults() {
return Object.assign({}, super.defaults, {
label: "Save",
templates: {
main: "<button part='button'></button>",
},
});
}
[initMethodSymbol]() {
super[initMethodSymbol]();
this.initialLabel = this.getOption("label");
}
[assembleMethodSymbol]() {
super[assembleMethodSymbol]();
this.shadowRoot.querySelector("button").textContent =
this.getOption("label");
}
}Programmatic configuration starts after construction
document.createElement() completes the constructor synchronously. Calling setOption() on the returned element is therefore safe before appending it. Attributes added only after createElement() were not available to the earlier constructor hooks; resolve connection-dependent values during assembly instead.
Common Mistakes
Skipping registration
Without registerCustomElement(), the tag never becomes a real custom element and the browser treats it as unknown markup.
Overriding lifecycle methods without super
That usually breaks internal setup, especially Shadow DOM and option initialization.
Reading options while defining them
Calling the options API from defaults or customization throws in Monster 5. Move derived state to initMethodSymbol and DOM-dependent work to assembly.
Choosing CustomElement when the control is really form-associated
If the element has a value, validation and form semantics, start from CustomControl instead.