Pulse · Alpine-style

Two-way binding

kj-model="r" keeps a control and a state property in sync — both ways. Move a slider and the state changes; change the state and the slider moves.

kj-model works with text, number, range, textarea, select, checkbox (booleans) and radio buttons (the checked radio's value). It never rewrites the field you are typing in, but programmatic changes — a preset button, a reset — update it immediately.

The mixer below binds the hex field to a getter/setter pair: reading hex computes it from the three channels, writing a valid hex parses it back into them. That is all two-way binding needs.

Live · Colour mixer with contrast check

View the source for this demo
HTML
<div kj-data="mixer" class="mx">
  <div class="mx-swatch" :style="'background:' + rgb() + ';color:' + ink()" role="img" :aria-label="'Colour ' + hex">
    <span kj-text="hex"></span>
  </div>
  <div class="mx-ctrls">
    <div class="field"><label for="mx-r">Red <span kj-text="r"></span></label><input id="mx-r" type="range" min="0" max="255" kj-model="r"></div>
    <div class="field"><label for="mx-g">Green <span kj-text="g"></span></label><input id="mx-g" type="range" min="0" max="255" kj-model="g"></div>
    <div class="field"><label for="mx-b">Blue <span kj-text="b"></span></label><input id="mx-b" type="range" min="0" max="255" kj-model="b"></div>
    <div class="field"><label for="mx-hex">Hex (type a value)</label><input id="mx-hex" type="text" kj-model="hex" spellcheck="false" style="font-family:var(--mono)"></div>
    <div class="row mx-presets" aria-label="Presets">
      <button type="button" @click="hex = '#4D7CFF'" style="background:#4D7CFF" aria-label="Core blue"></button>
      <button type="button" @click="hex = '#FF4F8B'" style="background:#FF4F8B" aria-label="Hyper pink"></button>
      <button type="button" @click="hex = '#22CFA2'" style="background:#22CFA2" aria-label="Pulse mint"></button>
      <button type="button" @click="hex = '#FFB224'" style="background:#FFB224" aria-label="Motion amber"></button>
    </div>
    <p aria-live="polite" style="margin:0" kj-text="'White text contrast: ' + contrast() + ':1 ' + (contrast() >= 4.5 ? '✓ passes AA' : '✗ below AA')"></p>
  </div>
</div>
JavaScript
$.component('mixer', () => ({
  r: 108, g: 92, b: 231,
  get hex() {
    return '#' + [this.r, this.g, this.b].map(n => (+n).toString(16).padStart(2, '0')).join('').toUpperCase();
  },
  set hex(value) {                                       // typing a valid hex updates all three sliders
    const m = /^#?([0-9a-f]{6})$/i.exec(String(value).trim());
    if (m) [this.r, this.g, this.b] = [0, 2, 4].map(i => parseInt(m[1].slice(i, i + 2), 16));
  },
  rgb() { return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; },
  luminance() {
    return [this.r, this.g, this.b]
      .map(v => { v /= 255; return v <= .03928 ? v / 12.92 : Math.pow((v + .055) / 1.055, 2.4); })
      .reduce((sum, v, i) => sum + v * [.2126, .7152, .0722][i], 0);
  },
  contrast() { return +(1.05 / (this.luminance() + .05)).toFixed(2); },
  ink() { return this.contrast() >= 4.5 ? '#FFFFFF' : '#13152B'; },
}));

More examples

Every control type

HTML
<input kj-model="email" type="email">
<input kj-model="qty" type="number">          <!-- string: use +qty for maths -->
<input kj-model="agree" type="checkbox">      <!-- boolean -->
<input kj-model="size" type="radio" value="m"> <input kj-model="size" type="radio" value="l">
<select kj-model="country">…</select>

API

kj-model="property"

Two-way binding for text, number and range inputs, textareas, selects, checkboxes (booleans) and radio buttons (the checked radio’s value). The field being typed in is never rewritten mid-typing; programmatic changes update it immediately.

Reference: kj-model →

Migrating

Coming from Alpine.jsx-model → kj-model. Values arrive as strings (use +value); Alpine's .number, .lazy and .debounce modifiers are not included.

Related features