Alpine
@voltage/alpine integrates Alpine.js into the JSX rendering pipeline. When any x-* attribute is detected in the rendered output, the Alpine script is included in the page automatically. Passing a JavaScript object to x-data serializes it to JSON at render time — no manual JSON.stringify needed.
Installation
Section titled “Installation”yarn add @voltage/alpine @voltage/asset-manager @voltage/jsxBasic usage
Section titled “Basic usage”Call forRoot() in any module that renders Alpine-powered components:
import { AlpineModule } from '@voltage/alpine';
@Module({ imports: [AlpineModule.forRoot()],})export class AppModule {}Use x-* attributes in JSX as you would in plain HTML. The script is included automatically:
<div x-data={{ open: false }}> <button x-on:click="open = !open">Toggle</button> <p x-show="open">Hello</p></div>x-data serialization
Section titled “x-data serialization”Pass a plain JavaScript object to x-data. The module serializes it to JSON before the element is rendered:
<form x-data={{ values: [], max: 10 }}> {/* rendered as: x-data='{"values":[],"max":10}' */}</form>This means you can construct initial state server-side and pass it directly:
async render(props: FilterProps) { const { filter } = props; const initialValues = await this.query.for(Filter).load(filter);
return ( <form x-data={{ values: initialValues, open: null }}> {/* Alpine picks up the server-provided state */} </form> );}TypeScript support
Section titled “TypeScript support”All x-* attributes are typed in JSX out of the box. No additional tsconfig changes are needed — the type declarations are included with the package:
<div x-data={{ open: false, count: 0 }} x-init="count = $el.querySelectorAll('li').length" x-bind:class="open ? 'visible' : 'hidden'" x-on:click="open = !open" x-show="open"> <span x-text="count" /></div>All attribute values are JSX.Resolvable<T> — they accept Promises and Observables in addition to plain values, consistent with the rest of the JSX package.
Common directives
Section titled “Common directives”{/* Reactive state */}<div x-data={{ open: false, filter: '' }}>
{/* Initialization */}<div x-init="open = true">
{/* Two-way binding on inputs */}<input x-model="filter" />
{/* Loop */}<template x-for="(item, index) in items"> <li x-text="item.label" /></template>
{/* Conditional visibility */}<div x-show="open" /> {/* stays in DOM, toggled with display:none */}<div x-if="open" /> {/* removed from DOM entirely */}
{/* Event handling */}<button x-on:click="open = !open">Toggle</button>
{/* Dynamic attributes */}<div x-bind:class="open ? 'active' : ''" />
{/* Element reference */}<input x-ref="search" />
{/* Parent-child state sharing via x-modelable */}<div x-modelable="open" x-data={{ open: false }}> {/* Parent can bind to this component's 'open' with x-model */}</div>HTMX morphing
Section titled “HTMX morphing”When using Alpine alongside HTMX, partial page updates can destroy Alpine state because HTMX replaces DOM nodes by default. The HTMXAlpineMorphExtension tells HTMX to use Alpine’s morphing algorithm instead, which patches the DOM in place and preserves component state.
Import it from @voltage/htmx/alpine and pass it to HTMXModule.forRoot():
import { HTMXModule } from '@voltage/htmx';import { HTMXAlpineMorphExtension } from '@voltage/htmx/alpine';
HTMXModule.forRoot({ extensions: [HTMXAlpineMorphExtension],})Then use hx-swap="morph" on elements where state preservation matters:
<div x-data={{ count: 0 }} hx-get={this.url.generate(CounterController)} hx-swap="morph"> <span x-text="count" /></div>