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.

Tutorial Goal

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.

Hello!

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. Use them only when the normal lifecycle methods are not precise enough.

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

class MyButton extends CustomElement {
    [initMethodSymbol]() {
        super[initMethodSymbol]();
        // one-time initialization
    }

    [assembleMethodSymbol]() {
        super[assembleMethodSymbol]();
        // DOM-dependent assembly
    }
}

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.

Choosing CustomElement when the control is really form-associated

If the element has a value, validation and form semantics, start from CustomControl instead.

From Tutorial to Real Control

CustomElement to LocalePicker

Look at a component with runtime behavior but without form value semantics

LocalePicker is a good next stop after this tutorial. It has its own template, internal assembly, event handling and Shadow DOM behavior, but it is not primarily about form-associated value handling. That makes it a clear CustomElement-style component instead of a CustomControl.

What to inspect

Check registration, assembly and public behavior

Pay attention to how the tag is registered, how the template is assembled and which public methods and callbacks are exposed. The useful question here is not “what DOM did it render?” but “what stable outer contract does it give the page?”

Where to Go Next

Read the base class API

Continue into the reference page when you need the full option, method and symbol contract.

Move up to CustomControl

If you are building value-bearing controls, the next conceptual step is the form-aware control layer.

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