Migrating from Alpine.js

Alpine.js is the framework closest to WildflowerJS. Both are attribute-based, no-build-step frameworks that enhance HTML with reactive behavior. WildflowerJS adds a full component lifecycle, stores with subscriptions, cached computed properties, keyed list reconciliation, and a component model built for larger applications.

The approach is the same: if you know Alpine, you already understand how WildflowerJS works. The migration is mostly moving inline logic into component definitions and swapping x-* prefixes for data-* equivalents. Your HTML structure stays close to what it was.

Mental Model Shift

Both frameworks share the same core idea: enhance HTML with attributes instead of replacing it with JSX or custom file formats. But they diverge in how they organize logic and scale.

Concept Alpine.js WildflowerJS
Philosophy "Sprinkle" interactivity on existing pages Scales from sprinkles to full SPAs
State & logic Inline in x-data attribute Separate JS definitions (clean separation of concerns)
Getters / computed Re-evaluate on every access Cached, only recalculates when dependencies change
List rendering x-for with template expressions data-list with keyed reconciliation
Template element Uses <template> Uses <template> (same concept)
Component reuse Alpine.data() or inline Named components with full lifecycle
Global state Alpine.store() wildflower.store() with subscriptions
Scaling path Limited, designed for enhancement Full SPA architecture with routing, nested components, SSR
What transfers directly: Both frameworks bind with attributes and use <template> elements, so the HTML structure of your Alpine components translates almost directly to WildflowerJS. The main work is extracting inline JavaScript into component definitions.

Attribute Mapping

A direct translation table for every Alpine directive:

Alpine.js WildflowerJS Notes
x-data="{ state }" data-component="name" State defined in JS, not inline
x-text="expr" data-bind="prop" Text content binding
x-html="expr" data-bind-html="prop" Raw HTML binding
@click="expr" data-action="method" Logic moves to named JS methods
@click.prevent data-action="method" Event is passed to method; call event.preventDefault() there
@keydown.enter="expr" data-action="keydown.enter:method" Event modifiers use event.key:method syntax
x-model="prop" data-model="prop" Two-way binding, nearly identical
x-show="cond" data-show="cond" CSS display toggle, nearly identical
x-if="cond" data-render="cond" DOM insertion/removal
x-for="item in items" data-list="items" + <template> child Template goes inside the list element
:class="expr" data-bind-class="expr" Dynamic class binding
:style="expr" data-bind-style="expr" Dynamic style binding
x-init init() lifecycle hook Defined in component JS definition
x-effect Computed properties or init() Derived values use computed; side effects use init()
Alpine.store() wildflower.store() Very similar API
$store.name $name.prop or subscribe Cross-entity access in HTML, or subscribe pattern in JS

Side-by-Side: Todo List

Here's the same todo app in both frameworks:

Alpine.js

<div x-data="{
    todos: [],
    newTodo: '',
    get remaining() {
        return this.todos.filter(t => !t.done).length;
    },
    addTodo() {
        if (!this.newTodo.trim()) return;
        this.todos.push({
            id: Date.now(),
            text: this.newTodo,
            done: false
        });
        this.newTodo = '';
    },
    removeTodo(index) {
        this.todos.splice(index, 1);
    }
}">
    <h3>Todos (<span x-text="remaining"></span> remaining)</h3>
    <input x-model="newTodo" @keydown.enter="addTodo()" />
    <button @click="addTodo()">Add</button>
    <ul>
        <template x-for="(todo, index) in todos" :key="todo.id">
            <li>
                <input type="checkbox" x-model="todo.done" />
                <span x-text="todo.text"
                      :style="{ textDecoration: todo.done
                          ? 'line-through' : 'none' }"></span>
                <button @click="removeTodo(index)">&times;</button>
            </li>
        </template>
    </ul>
</div>

State, getters, and methods all live inline in the HTML attribute.

WildflowerJS

<div data-component="todo-app">
    <h3>Todos (<span data-bind="remaining">0</span> remaining)</h3>
    <input data-model="newTodo"
           data-action="keydown.enter:addTodo" />
    <button data-action="addTodo">Add</button>
    <ul data-list="todos" data-key="id">
        <template>
            <li>
                <input type="checkbox" data-model="done" />
                <span data-bind="text"
                      data-bind-style="{ textDecoration: done ? 'line-through' : 'none' }"></span>
                <button data-action="removeTodo">&times;</button>
            </li>
        </template>
    </ul>
</div>
wildflower.component('todo-app', {
    state: { todos: [], newTodo: '' },
    computed: {
        remaining() {
            return this.todos.filter(t => !t.done).length;
        }
    },
    addTodo() {
        if (!this.newTodo.trim()) return;
        this.todos.push({
            id: Date.now(),
            text: this.newTodo,
            done: false
        });
        this.newTodo = '';
    },
    removeTodo(event, element, details) {
        this.todos.splice(details.index, 1);
    }
});

The HTML is plain markup, and the logic lives in a separate JS definition.

Key differences to notice:
  • Alpine puts state inline in x-data; WildflowerJS uses a named data-component with JS definition
  • Alpine's get remaining() re-evaluates on every access; WildflowerJS's computed caches the result
  • Alpine's x-for uses (todo, index) in todos; WildflowerJS's data-list gets index from the action details parameter
  • The <template> in WildflowerJS goes inside the data-list element, not as a sibling

Pattern-by-Pattern Migration

Component Definition

Alpine inlines everything in x-data. WildflowerJS separates markup from behavior.

Alpine
<div x-data="{
    count: 0,
    get doubled() { return this.count * 2; },
    increment() { this.count++; }
}">
    <span x-text="doubled"></span>
    <button @click="increment()">+</button>
</div>
WildflowerJS
<div data-component="counter">
    <span data-bind="doubled"></span>
    <button data-action="increment">+</button>
</div>
wildflower.component('counter', {
    state: { count: 0 },
    computed: {
        doubled() { return this.count * 2; }
    },
    increment() { this.count++; }
});

State

Alpine's state is an inline object. WildflowerJS uses a state property in the component definition.

Alpine
<div x-data="{ name: '', email: '', agreed: false }">
    ...
</div>
WildflowerJS
wildflower.component('signup-form', {
    state: {
        name: '',
        email: '',
        agreed: false
    }
});
<div data-component="signup-form">...</div>

Computed / Getters

Alpine uses JavaScript getter syntax that re-evaluates on every access. WildflowerJS computed properties are cached and only recalculate when their dependencies change.

Alpine
<div x-data="{
    items: [],
    get total() {
        // Runs EVERY time 'total' is read
        return this.items.reduce((s, i) => s + i.price, 0);
    },
    get expensive() {
        return this.items.filter(i => i.price > 100);
    }
}">
    <span x-text="total"></span>
</div>
WildflowerJS
wildflower.component('cart', {
    state: { items: [] },
    computed: {
        // Cached! Only re-runs when items changes
        total() {
            return this.items.reduce((s, i) => s + i.price, 0);
        },
        expensive() {
            return this.items.filter(i => i.price > 100);
        }
    }
});
Caching difference: If your Alpine component has expensive getters that get accessed multiple times per render cycle, WildflowerJS computed properties will only calculate once and cache the result until dependencies change.

Event Handling

Alpine allows inline JavaScript expressions in event handlers. WildflowerJS uses named methods.

Alpine
<!-- Inline expression -->
<button @click="count++">+</button>

<!-- Method call -->
<button @click="handleSubmit()">Submit</button>

<!-- With modifiers -->
<form @submit.prevent="save()">
<button @click.stop="close()">X</button>

<!-- Key modifiers -->
<input @keydown.enter="search()" />
<input @keydown.escape="clear()" />
WildflowerJS
<!-- Named method -->
<button data-action="increment">+</button>

<!-- Same pattern -->
<button data-action="handleSubmit">Submit</button>

<!-- Modifiers in the method -->
<form data-action="submit:save">
<button data-action="close">X</button>

<!-- Key modifiers -->
<input data-action="keydown.enter:search" />
<input data-action="keydown.escape:clear" />
// Handle event modifiers in JS
save(event) {
    event.preventDefault();
    // save logic...
},
close(event) {
    event.stopPropagation();
    // close logic...
}

Conditional Rendering

Both frameworks have show/hide and insert/remove patterns. The syntax is nearly identical.

Alpine
<!-- CSS toggle (stays in DOM) -->
<div x-show="isOpen">Dropdown content</div>

<!-- DOM insertion/removal -->
<template x-if="isLoggedIn">
    <div>Welcome back!</div>
</template>
WildflowerJS
<!-- CSS toggle (stays in DOM) -->
<div data-show="isOpen">Dropdown content</div>

<!-- DOM insertion/removal -->
<div data-render="isLoggedIn">Welcome back!</div>
Note: Alpine's x-if requires a wrapping <template> element. WildflowerJS's data-render goes directly on the element you want to conditionally render, with no wrapper.

List Rendering

Both use <template> elements, but the structure differs.

Alpine
<ul>
    <template x-for="item in items" :key="item.id">
        <li>
            <span x-text="item.name"></span>
            <span x-text="item.price"></span>
        </li>
    </template>
</ul>

The template is a sibling or child of the list container, and the key is on the template.

WildflowerJS
<ul data-list="items" data-key="id">
    <template>
        <li>
            <span data-bind="name"></span>
            <span data-bind="price"></span>
        </li>
    </template>
</ul>

The template goes inside the data-list element, the key is on the list container, and bindings need no item. prefix.

Keyed reconciliation: WildflowerJS's data-key enables efficient DOM reuse when items are reordered, inserted, or removed, similar to React's key prop but with true keyed reconciliation instead of Alpine's full re-render.

Forms

Two-way binding is nearly identical between the frameworks.

Alpine
<input type="text" x-model="username" />
<textarea x-model="bio"></textarea>
<select x-model="role">
    <option value="admin">Admin</option>
    <option value="user">User</option>
</select>
<input type="checkbox" x-model="agreed" />
WildflowerJS
<input type="text" data-model="username" />
<textarea data-model="bio"></textarea>
<select data-model="role">
    <option value="admin">Admin</option>
    <option value="user">User</option>
</select>
<input type="checkbox" data-model="agreed" />

Stores

Both frameworks have a global store concept, but WildflowerJS adds declarative subscriptions with automatic cleanup.

Alpine
Alpine.store('cart', {
    items: [],
    get total() {
        return this.items.reduce((s, i) => s + i.price, 0);
    },
    add(item) {
        this.items.push(item);
    }
});
<!-- Access in any component -->
<span x-text="$store.cart.total"></span>
<button @click="$store.cart.add(item)">Add</button>
WildflowerJS
wildflower.store('cart', {
    state: { items: [] },
    computed: {
        total() {
            return this.items.reduce((s, i) => s + i.price, 0);
        }
    },
    add(item) {
        this.items.push(item);
    }
});
<!-- Access in any component -->
<span data-bind="$cart.total"></span>
// Or subscribe in a component definition
wildflower.component('checkout', {
    subscribe: { cart: ['items'] },
    onStoreUpdate(store, path, newVal) {
        // React to store changes
    }
});

Scaling Up

Alpine is designed for "sprinkling" interactivity. WildflowerJS covers the same case and also handles full single-page applications.

Alpine (typical usage)
<!-- Individual interactive widgets -->
<div x-data="{ open: false }">
    <button @click="open = !open">Menu</button>
    <nav x-show="open">...</nav>
</div>

<div x-data="searchWidget()">...</div>
<div x-data="cartWidget()">...</div>

<!-- No routing, no nesting, no SSR -->
WildflowerJS (full SPA)
<!-- App shell with routing -->
<div data-component="app-shell">
    <nav data-component="main-nav">...</nav>
    <main data-route="/dashboard">
        <div data-component="dashboard">
            <div data-component="stats-panel">...</div>
            <div data-component="activity-feed">...</div>
        </div>
    </main>
</div>

<!-- Nested components, routing, stores, SSR -->

Common Gotchas

No Inline State

State and logic live in a JavaScript file, not in HTML attributes. There is no equivalent to x-data="{ count: 0 }" with inline state. Always define a named component with wildflower.component('name', { state: {...} }).

No Inline Expressions in Actions

Alpine lets you write @click="count++". WildflowerJS requires named methods:

<!-- Alpine -->
<button @click="count++">+</button>
<button @click="items.push({id: Date.now()})">Add</button>

<!-- WildflowerJS -->
<button data-action="increment">+</button>
<button data-action="addItem">Add</button>
Computed Properties Are Cached

Unlike Alpine getters that re-run every time they are accessed, WildflowerJS computed properties only recalculate when their dependencies change. This is a feature, not a bug. It means expensive computations like filtering or reducing large arrays happen much less often.

List Template Is a Child, Not a Sibling

The <template> element goes inside the data-list element:

<!-- Alpine: template is a direct child of the container -->
<ul>
    <template x-for="item in items"><li x-text="item.name"></li></template>
</ul>

<!-- WildflowerJS: data-list goes on the container, template inside -->
<ul data-list="items">
    <template><li data-bind="name"></li></template>
</ul>
No $refs or $dispatch

Alpine provides magic properties like $el, $refs, and $dispatch. WildflowerJS has its own this.$el(selector), a jQuery-like DOM helper for querying within the component (see DOM Helpers). There are no $refs or $dispatch equivalents. Instead, action methods receive the event and element as parameters: myMethod(event, element, details). For cross-component communication, use stores with subscriptions or the $name.prop cross-entity access syntax in HTML.

Component Registration Is Required

You must call wildflower.component('name', {...}) before using data-component="name" in HTML. Alpine's x-data works inline without any prior registration. Think of WildflowerJS components like Vue or React components. They must be defined before they are used.

Scaling Beyond Alpine

WildflowerJS starts from the same place, lightweight and buildless, and adds the following:

Component Lifecycle

Alpine has x-init but no destroy hook. WildflowerJS provides a full lifecycle:

wildflower.component('live-feed', {
    state: { messages: [] },
    init() {
        // Runs when component mounts
        this._ws = new WebSocket('wss://api.example.com/feed');
        this._ws.onmessage = (e) => {
            this.messages.push(JSON.parse(e.data));
        };
    },
    destroy() {
        // Runs when component unmounts: no memory leaks
        this._ws.close();
    }
});

Store Subscriptions

Alpine stores are global reactive objects. WildflowerJS stores add declarative subscriptions with automatic cleanup:

wildflower.component('notification-badge', {
    state: { unreadCount: 0 },
    subscribe: {
        notifications: ['items']  // Watch notifications store
    },
    onStoreUpdate(storeName, path, newValue) {
        if (storeName === 'notifications') {
            this.unreadCount = newValue.filter(n => !n.read).length;
        }
    }
    // Subscription is automatically cleaned up on component destroy
});

SPA Routing

Alpine has no built-in routing. WildflowerJS includes a full RouteManager:

<nav data-component="main-nav">
    <a href="/dashboard">Dashboard</a>
    <a href="/settings">Settings</a>
</nav>

<main>
    <div data-route="/dashboard" data-component="dashboard">...</div>
    <div data-route="/settings" data-component="settings">...</div>
</main>

Nested Components and Communication

Alpine components are isolated islands. WildflowerJS supports nested components with props and slots:

<div data-component="user-profile">
    <div data-component="avatar-card" data-props="{ name: userName, image: userAvatar }">
        <img data-bind-attr="{ src: props.image }" />
        <span data-bind="props.name"></span>
    </div>
    <div data-component="activity-timeline" data-props="{ userId: id }">
        ...
    </div>
</div>

Server-Side Rendering

WildflowerJS includes an SSR manager for rendering components on the server with hydration on the client, something Alpine was never designed for.

Migration Checklist

Follow these steps to migrate an Alpine.js project to WildflowerJS:

  1. Extract inline x-data objects into wildflower.component() definitions in a separate JS file
  2. Replace x-* attributes with data-* equivalents using the attribute mapping table above
  3. Move inline event handlers (@click="count++") to named methods in the component definition
  4. Convert Alpine.store() calls to wildflower.store() with explicit state and computed blocks
  5. Add lifecycle hooks where needed: replace x-init with init(), add destroy() for cleanup
  6. Add data-key to list containers for efficient keyed reconciliation
  7. Restructure as nested components for larger pages: break monolithic x-data blocks into composable components
  8. Test each component after migration: verify state, events, computed values, and list rendering all work correctly