Web Components
A Pagelove Web Component is a real custom element, auto-defined by pagelove.mjs from a <template itemtype> whose root tag is hyphenated, extending the PageloveComponent base class so its light-DOM view stays in sync with its backing schema article.
When to reach for it
Reach for a Web Component when a template is a live, interactive element with its own lifecycle — drag, resize, mixin behavior, or any API the rest of the page will call. Give the template root a hyphenated tag (<sticky-note>, <kanban-card>) and the runtime takes care of the rest.
Auto-defined custom elements
On boot, pagelove.mjs walks every <template itemtype> it has discovered and inspects template.content.firstElementChild. The auto-definition rule is:
| Condition | Result |
|---|---|
Root tag contains a - and is not already registered |
A class extending PageloveComponent is defined via customElements.define, with static itemtype set to the template's itemtype. |
Root tag contains no - |
Skipped — renders as a plain DOM fragment. |
| Root tag already registered | Skipped — the existing registration wins, so apps can pre-register subclasses before pagelove.mjs runs. |
| Same tag is the root of two templates with different itemtypes | The runtime throws — a custom element can only represent one schema type. |
Any mixins whose selectors match the template root are stacked onto the base class before customElements.define is called.
The PageloveComponent base class
PageloveComponent extends HTMLElement. It is an autonomous custom element — not a built-in extension — so the root tag itself is what the browser upgrades.
| Member | Purpose |
|---|---|
static itemtype |
Set by the auto-definer to the schema itemtype the class represents. null on the unsubclassed base. |
static binders |
Array of binder definitions. Ships with dataBindBinder and dataBindAttrBinder. |
static create(values) |
Factory that calls window.pagelove.create(this.itemtype, values) to build a new schema article. |
connectedCallback() |
Resolves the schema article from data-for, builds a propName → [{element, binder}] index from every binder's collect(), runs an initial bind, then starts a MutationObserver on the schema article. |
disconnectedCallback() |
Disconnects the observer, clears the index, drops the schema article reference. |
_get(propName) / _set(propName, value) |
Read and write schema properties. Used by mixins and subclasses. |
The observer watches childList, subtree, characterData, and attributes (filtered to content and datetime). When a schema property becomes dirty, every indexed entry is re-applied through its binder, so schema writes flow one-way into the view.
The default binders
dataBindBinder collects every [data-bind] element under the component root and writes the named schema property to either element.value (for form controls) or element.textContent (for everything else).
dataBindAttrBinder reads the root's data-bind-attr attribute, splits on whitespace, and forwards each named property onto root.dataset.<propName> so CSS and selectors can react to schema values.
Both binders implement the rules documented on the HTML bindings page — the base class simply runs them inside its own lifecycle.
Mixins
Mixins add behavior to auto-defined components by wrapping the PageloveComponent base class before it is registered. The registry is a module-level array of (selector, mixinFn) pairs.
| Function | Signature | Purpose |
|---|---|---|
registerComponentMixin |
(selector, mixinFn) |
Push a new entry. selector is a CSS selector tested against the template root; mixinFn is a class factory of the form (Base) => class extends Base { … }. |
getMatchingMixins |
(rootElement) |
Return the mixin factories whose selector matches the given root. Rarely called directly — the auto-definer uses it during discovery. |
Stacking order is governed by reduceRight: mixins are applied in reverse registration order, so the earliest-registered mixin sits closest to PageloveComponent and later registrations wrap around it. Each mixin should call super.connectedCallback?.() and super.disconnectedCallback?.().
Built-in mixins
Three mixins ship with the component module and auto-register themselves on import.
| Mixin | Auto-registered selector | Reads / writes | What it does |
|---|---|---|---|
Draggable |
[data-draggable] |
writes data-x, data-y |
Pointer-driven positioning. On pointerdown (or inside a [data-drag-handle] descendant if one exists), it tracks the pointer and updates inline left / top for live feedback. On release it writes dataset.x / dataset.y, which the runtime commits to the schema as x / y. |
Resizable |
[data-resizable] |
writes data-width, data-height |
Injects a .pl-resize-handle corner child and drives resize via pointer events. On release it writes dataset.width / dataset.height, which the runtime commits to the schema as width / height. |
Stackable |
[data-stackable] |
writes data-z |
Click-to-front z-ordering. On pointerdown (capture phase, non-intrusive), it sets data-z to one more than the maximum data-z of its siblings, which the runtime commits to the schema as z. |
Stackable also mirrors its data-z attribute to inline style.zIndex on connection and whenever the attribute changes. This keeps the stacking applied without depending on CSS attr(data-z integer), which Safari does not yet support.
Examples
Minimal custom element from a template
<template itemtype="https://schema.host/StickyNote">
<sticky-note>
<h1 data-bind="title" contenteditable></h1>
<p data-bind="body" contenteditable></p>
</sticky-note>
</template>
The root is hyphenated, so pagelove.mjs defines <sticky-note> as a PageloveComponent subclass. Each stamped instance binds title and body through dataBindBinder.
Draggable, resizable, and stackable without writing any JavaScript
<template itemtype="https://schema.host/StickyNote">
<sticky-note data-draggable data-resizable data-stackable>
<h1 data-bind="title" contenteditable></h1>
<p data-bind="body" contenteditable></p>
</sticky-note>
</template>
All three built-in selectors match the template root, so the auto-definer stacks Draggable, Resizable, and Stackable onto PageloveComponent. Dragging the note commits x and y to the schema; dragging the corner handle commits width and height; clicking the note raises z so it sits above its siblings.
A custom mixin
import { registerComponentMixin } from 'https://pagelove.github.io/beta-js/pagelove/component.mjs';
registerComponentMixin('[data-glow]', (Base) => class extends Base {
connectedCallback() {
super.connectedCallback?.();
this.addEventListener('pointerenter', () => this.classList.add('glowing'));
this.addEventListener('pointerleave', () => this.classList.remove('glowing'));
}
disconnectedCallback() {
super.disconnectedCallback?.();
this.classList.remove('glowing');
}
});
Any template root that matches [data-glow] now gets hover-glow behavior as part of its class chain, provided registration happens before pagelove.mjs discovers templates.
See also
- The Pagelove class — the runtime that discovers templates and calls the component auto-definer.
- HTML bindings — the binding rules implemented by the default
dataBindBinderanddataBindAttrBinder. - Declarative commands —
--create-instanceand--remove-instancebuttons hosted inside template-defined components.