Alpine JS does not have a standard pattern for implementing web components. Therefore the library uses its own approach. Each component follows this pattern for creating Alpine JS-based custom elements.
import Alpine from 'alpinejs';
import {
AashComponent, Attribute, Renderer, booleanConverter, functionConverter
} from '../../AashUtil';
@AashComponent()
export class MyComponent extends HTMLElement {
private readonly ajsData: MyComponentData;
constructor() {
super();
this.ajsData = Alpine.reactive({
someProperty: '',
someMethod(): void { ... }
} satisfies MyComponentData);
}
@Data()
protected data() {
return this.ajsData;
}
@Renderer()
protected render() {
this.innerHTML = `<span x-text="someProperty"></span>`;
}
@Attribute('some-prop')
set someProperty(value: string) {
this.ajsData.someProperty = value;
}
}
customElements.define('my-component', MyComponent);
Reactive data — ajsData holds an Alpine.reactive() object
containing all state and methods that Alpine templates reference.
Lifecycle — connectedCallback (provided by the class decorator)
sets the attribute x-data (if missing) to the object returned
by the method decorated with @Data(). It then calls the method
decorated with @Renderer(). If the component defines its own
connectedCallback, the decorator invokes it before setting the
attribute. This allows the component to initialize controller state
before rendering.
Data — The method decorated with @Data() is called to get the
object with the component's data (Alpine's x-data attribute).
Render — The method decorated with @Renderer() creates the
innerHTML of the web component. The innerHTML uses Alpine directives
(x-text, x-bind, x-for, x-show, x-init, x-cloak, etc.) for
reactive rendering. The directives can reference the data provided
for the instance by the method decorated with @Data().
Property setters — Public setters mutate the component's data,
triggering Alpine's reactivity. Use @Attribute to bind setters to HTML
attributes. For programmatic API usage, expose plain setters without
@Attribute (e.g. onAction on AashModalDialog).
Cleanup — Override disconnectedCallback to release resources
(remove event listeners, disconnect MutationObservers). The
decorator chains the user's callback after its own setup, so
cleanup code should not interfere with the decorator's lifecycle.
@AashComponent()Class decorator that generates:
static get observedAttributes() — lists all @Attribute decorated
settersattributeChangedCallback(name, oldVal, newVal) — converts attribute
string via the setter's converter and calls the setterconnectedCallback — calls the component's
own connectedCallback if it exists and then sets the attributes
x-data (if missing).Usage:
@AashComponent()
class MyComponent extends HTMLElement { ... }
@Attribute(name?, converter?)Method decorator on a setter.
name — HTML attribute name. Defaults to the setter property name.converter — Converter object with fromAttribute and toAttribute
methods. Defaults to defaultConverter (auto-detects type).Usage:
/* Default converter auto-detects string/number/boolean/object/array */
@Attribute('my-prop')
set myProp(value: string) { this.ajsData.myProp = value; }
/* Number auto-detected from attribute value "42" */
@Attribute()
set count(value: number) { this.ajsData.count = value; }
/* Boolean via "true"/"false" string or presence-based */
@Attribute('enabled')
set enabled(value: boolean) { this.ajsData.enabled = value; }
@Attribute('show-cancel', booleanConverter)
set showCancel(value: boolean) { this.ajsData.showCancel = value; }
/* Object/array auto-detected from JSON in attribute value */
@Attribute('config')
set config(value: object) { this.ajsData.config = value; }
/* Function converter for callback attributes */
@Attribute('on-action', expressionConverter)
set onAction(value: ((...args: any[]) => any) | null) {
this.ajsData.onAction = value;
}
| Converter | Type | fromAttribute | toAttribute |
|---|---|---|---|
defaultConverter |
auto-detect | see below | type-dependent |
booleanConverter |
boolean |
v !== null (presence) |
v ? '' : null |
expressionConverter |
fn || null |
Alpine-evaluated | v.toString() |
expressionConverter converts the attribute string to a function. If called
with an HTMLElement argument (as done by the framework internally), it uses
Alpine.evaluate(el, value) to evaluate the expression within Alpine's
reactive context, allowing references to Alpine data properties. Without an
element argument it falls back to new Function(...).
Typical usage for callback attributes:
@Attribute('on-action', expressionConverter)
set onAction(value: ((...args: any[]) => any) | null) {
this.ajsData.onAction = value;
}
defaultConverter auto-detects type from the attribute value:
| Value pattern | Result type |
|---|---|
null / absent |
'' (empty string) |
starts with [ |
JSON array |
starts with { |
JSON object |
literal true / false |
boolean |
matches -?\d+(\.\d+)? |
number |
| anything else | string |
For presence-based boolean attributes (e.g. show-cancel) use
booleanConverter instead.
@Data()Method decorator that marks a method as the supplier for the componet's data.
@Renderer()Method decorator that marks a method as the component's renderer.
Usage:
@Renderer()
protected render() {
this.innerHTML = `<span x-text="someProperty"></span>`;
}
Components that embed user-provided HTML fragments use a pattern based on
<template> elements and AlpineJS's x-teleport directive.
The consumer places <template> children inside the web component with a
provides attribute to declare what part of the widget the content fills:
<aash-dropdown-menu id="lang-select">
<template provides="label">
<span>Language</span>
</template>
<template provides="item" with-value="en">
<span>English</span>
</template>
<template provides="item" with-value="de">
<span>German</span>
</template>
</aash-dropdown-menu>
The provides attribute tells the component which role the template
serves. Additional attributes (e.g. with-value) are component-specific.
In render the component creates its structural HTML, assigning
a unique id (via aashId()) to every element that should receive user
content:
const labelTarget = aashId();
this.prepend(
this.ownerDocument.createRange().createContextualFragment(
`<button id="${labelTarget}" aria-haspopup="menu">
</button>`));
For repeated slots (e.g. menu items), the preferred approach is to
use a <template x-for=...> in the template that generates the component's
DOM. The value that x-for iterates over must be reactive data and is
updated by updateContent() based on the <template provides="..."> children
found.
The x-init directive of elements generated in the loop is used to
set an x-teleport attribute pointing to the corresponding target.
The teleport must be scheduled via Alpine.nextTick() because setting
x-teleport during x-init evaluation is too early — Alpine has not
yet registered the target element. The teleportItem method in
AashDropdownMenu demonstrates this pattern.
Alpine's x-teleport moves the template's inner content into the target
element at initialization time (when the render method is called).
If the component supports dynamic changes of the injected content,
it must define a MutationObserver watching childList. The observer
re-runs updateContent() when templates are added or removed,
keeping the rendered structure in sync.
To avoid the observer firing on the DOM mutations caused by updateContent
itself, the observer must be disconnected before the update and re-connected
afterward.
For non-repeated slots (a single title, label, or content area), the
component sets x-teleport on the <template> directly in
updateContent(). The target element gets its content cleared first
so the teleported content replaces any default text. AashModalDialog
demonstrates this for its title, cancel-label, apply-label, okay-label,
and content slots.
<template> elements.aashId() so teleported content goes to the right place.childList: true on the host
catches added/removed templates without triggering on inner changes.npm run build
Output lands in lib/ and is consumed by the provider module
org.jgrapes.webconsole.provider.aashalpinejs.