Pulse · Alpine-style

Reactive state

Put state on an element with kj-data. Everything inside can read it, change it, and re-render when it changes. The tip calculator below has zero lines of JavaScript.

How it works

The kj-data object is wrapped in a Proxy. When a property changes, the component re-evaluates its bindings (kj-text, kj-show, :attr, kj-model). Methods defined on the object can be called from any expression, and this refers to the state.

Nested components get their own scope; an element binds to its nearest kj-data ancestor. If the object has an init() method, it runs once when the component mounts and receives the root element.

Expressions are evaluated with new Function, like Alpine's standard build, so pages using Pulse need 'unsafe-eval' if you run a strict Content-Security-Policy.

Live · Tip calculator — zero JavaScript

People
Each pays
View the source for this demo
HTML
<div class="tipcalc" kj-data="{
    bill: 84, tip: 18, people: 3,
    total() { return +this.bill * (1 + this.tip / 100); },
    each()  { return this.total() / Math.max(1, +this.people); }
  }">
  <div class="field"><label for="tc-bill">Bill ($)</label><input id="tc-bill" type="number" min="0" step="0.01" kj-model="bill"></div>
  <div class="field"><label for="tc-tip">Tip: <span kj-text="tip + '%'"></span></label><input id="tc-tip" type="range" min="0" max="30" kj-model="tip"></div>
  <div class="field"><span style="font-weight:600;font-size:14.5px">People</span>
    <div class="row">
      <button type="button" @click="people = Math.max(1, +people - 1)" aria-label="Fewer people">−</button>
      <output kj-text="people" aria-live="polite"></output>
      <button type="button" @click="people = +people + 1" aria-label="More people">+</button>
    </div></div>
  <div class="tc-result" aria-live="polite"><span>Each pays</span><strong kj-text="'$' + each().toFixed(2)"></strong><small kj-text="'Total $' + total().toFixed(2) + ' incl. tip'"></small></div>
</div>

More examples

State, methods and init()

HTML
<div kj-data="{ open: false, items: [], async init() { this.items = await $.getJSON('/api/items'); } }">
  <button @click="open = !open" :aria-expanded="open">Menu</button>
  <ul kj-show="open" kj-html="items.map(i => '<li>' + $.escape(i.name) + '</li>').join('')"></ul>
</div>

API

kj-data="{ … }" | kj-data="componentName"

Declares a component and its reactive scope. An init() method, if present, runs on mount. Pass a registered component name to reuse logic.

Reference: kj-data →
$.reactive(object) → proxy

A standalone reactive proxy without auto-rendering — for your own change-tracking logic.

Reference: $.reactive() →

Migrating

Coming from Alpine.jsx-data → kj-data, same object syntax and init(). Alpine's magic properties ($watch, $dispatch, $refs, $nextTick) are not included; $el and $event are.

Related features