Migrating from Alpine to Sprae

Quick Reference

Alpine Sprae
x-data="{ count: 0 }" :scope="{ count: 0 }"
x-text="message" :text="message"
x-html="content" :html="content"
x-show="open" :hidden="!open"
x-if="condition" :if="condition"
x-for="item in items" :each="item in items"
x-bind:class="..." :class="..."
x-bind:disabled="..." :disabled="..."
x-on:click="..." :onclick="..."
@click="..." :onclick="..."
x-model="value" :value="value"
x-ref="name" :ref="name"
x-effect="..." :fx="..."
x-teleport="#target" :portal="'#target'"
x-cloak CSS: [\:scope] { visibility: hidden }
x-init="..." :fx.once="..." or :scope.once="..."
x-transition CSS transitions

Directives

Data/Scope

<!-- Alpine -->
<div x-data="{ count: 0, name: 'World' }">

<!-- Sprae -->
<div :scope="{ count: 0, name: 'World' }">

Text & HTML

<!-- Alpine -->
<span x-text="message"></span>
<div x-html="content"></div>

<!-- Sprae -->
<span :text="message"></span>
<div :html="content"></div>

Conditionals

<!-- Alpine -->
<template x-if="show">
  <div>Visible</div>
</template>
<template x-else>
  <div>Hidden</div>
</template>

<!-- Sprae -->
<div :if="show">Visible</div>
<div :else>Hidden</div>

<!-- or with template for fragments -->
<template :if="show">
  <div>Visible</div>
</template>

Show/Hidden

<!-- Alpine: x-show keeps element, toggles display -->
<div x-show="open">Content</div>

<!-- Sprae: :hidden toggles hidden attribute -->
<div :hidden="!open">Content</div>

<!-- Sprae: :if removes/adds element -->
<div :if="open">Content</div>

Loops

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

<!-- Sprae: key is automatic (item identity) -->
<li :each="item in items" :text="item.name"></li>

<!-- with index -->
<li :each="item, idx in items" :text="idx + ': ' + item.name"></li>

Events

<!-- Alpine -->
<button x-on:click="count++">Click</button>
<button @click="count++">Click</button>
<form @submit.prevent="save()">

<!-- Sprae -->
<button :onclick="count++">Click</button>
<form :onsubmit.prevent="save()">

<!-- Accessing event object -->
<!-- Alpine: $event magic -->
<input @input="search($event.target.value)">

<!-- Sprae: arrow function -->
<input :oninput="e => search(e.target.value)">

Attributes

<!-- Alpine -->
<input x-bind:disabled="loading">
<input :disabled="loading">
<div x-bind:class="{ active: isActive }">

<!-- Sprae -->
<input :disabled="loading">
<div :class="{ active: isActive }">

Model (Two-way Binding)

<!-- Alpine -->
<input x-model="query">
<select x-model="country">

<!-- Sprae -->
<input :value="query">
<select :value="country">

Refs

<!-- Alpine -->
<input x-ref="input">
<button @click="$refs.input.focus()">

<!-- Sprae -->
<input :ref="input">
<button :onclick="input.focus()">

Effects

<!-- Alpine -->
<div x-effect="console.log(count)">

<!-- Sprae -->
<div :fx="console.log(count)">

<!-- with cleanup -->
<div :fx="() => { const id = setInterval(tick, 1000); return () => clearInterval(id) }">

Teleport/Portal

<!-- Alpine -->
<template x-teleport="#modals">
  <div>Modal</div>
</template>

<!-- Sprae: selector must be quoted -->
<div :portal="'#modals'">Modal</div>

Modifiers

Alpine Sprae
.prevent .prevent
.stop .stop
.outside .away
.window .window
.document .document
.once .once
.debounce .debounce
.throttle .throttle
.self .self
.passive .passive
.capture .capture
<!-- Alpine -->
<button @click.prevent.stop="save()">

<!-- Sprae -->
<button :onclick.prevent.stop="save()">

No Direct Equivalent

x-init

Use :fx or initialize in :scope:

<!-- Alpine -->
<div x-init="fetchData()">

<!-- Sprae: via effect -->
<div :fx.once="fetchData()">

<!-- or in scope -->
<div :scope.once="fetchData(), { data }">

x-cloak

Sprae removes directive attributes after processing, so use CSS to hide unprocessed elements:

[\:scope], [\:if], [\:each], [\:text] { visibility: hidden }

This hides elements until sprae initializes them (removes the attributes).

$store

Use shared state object:

// Alpine
Alpine.store('user', { name: 'John' })
// <span x-text="$store.user.name">

// Sprae
const user = { name: 'John' }
sprae(el, { user })
// <span :text="user.name">

$watch

Use :fx:

<!-- Alpine -->
<div x-data x-init="$watch('count', v => console.log(v))">

<!-- Sprae -->
<div :scope :fx="console.log(count)">

$dispatch

Use native events:

<!-- Alpine -->
<button @click="$dispatch('notify', { message: 'Hello' })">

<!-- Sprae -->
<button :onclick="dispatchEvent(new CustomEvent('notify', { detail: { message: 'Hello' }}))">

$nextTick

Use queueMicrotask or setTimeout:

<!-- Alpine -->
<button @click="count++; $nextTick(() => console.log($refs.count.innerText))">

<!-- Sprae -->
<button :onclick="count++; queueMicrotask(() => console.log(countEl.innerText))">

CSP (Content Security Policy)

Alpine’s CSP build has limitations (no arrow functions, no nested property assignments).

Sprae with jessie supports full CSP compliance with more features:

import sprae from 'sprae'
import jessie from 'subscript/jessie'

sprae.use({ compile: jessie })
<!-- Works with jessie (fails in Alpine CSP) -->
<button :onclick="user.name = 'John'">Set Name</button>
<button :onclick="items.filter(i => i.active)">Filter</button>

Example Migration

Alpine

<div x-data="{ todos: [], newTodo: '' }">
  <input x-model="newTodo" @keydown.enter="todos.push({ text: newTodo, done: false }); newTodo = ''">
  <template x-for="todo in todos" :key="todo.text">
    <div>
      <input type="checkbox" x-model="todo.done">
      <span x-text="todo.text" :class="{ 'line-through': todo.done }"></span>
    </div>
  </template>
  <span x-text="todos.filter(t => !t.done).length + ' remaining'"></span>
</div>

Sprae

<div :scope="{ todos: [], newTodo: '' }">
  <input :value="newTodo" :onkeydown.enter="todos.push({ text: newTodo, done: false }); newTodo = ''">
  <div :each="todo in todos">
    <input type="checkbox" :value="todo.done">
    <span :text="todo.text" :class="{ 'line-through': todo.done }"></span>
  </div>
  <span :text="todos.filter(t => !t.done).length + ' remaining'"></span>
</div>