Pulse · Alpine-style

Lists, conditionals and teleport

<template kj-for="todo in todos" :key="todo.id"> renders one element per item and, with a key, moves existing nodes instead of rebuilding them.

Keyed lists

kj-for goes on a <template> with exactly one root element. It accepts item in items, (item, index) in items, objects ((value, key) in obj) and ranges (i in 5). Add :key so reordering moves DOM nodes — inputs keep their focus and typed text.

kj-if and kj-teleport

kj-if adds or removes its template's element entirely (unlike kj-show, which only hides it). kj-teleport="body" renders a template somewhere else — useful for modals that must escape an overflow: hidden parent — while keeping the component's state and event handlers.

Live · Keyed to-do list with a teleported dialog

View the source for this demo
HTML
<div kj-data="{ next: 4, draft: '', confirm: false,
  todos: [{ id: 1, text: 'Read the docs', done: true }, { id: 2, text: 'Try a demo', done: false }, { id: 3, text: 'Ship it', done: false }] }">
  <form class="row" @submit.prevent="if (draft.trim()) { todos.push({ id: next++, text: draft.trim(), done: false }); draft = '' }">
    <div class="field" style="flex:1"><label for="lr-new">New item</label><input id="lr-new" kj-model="draft" placeholder="Something to do"></div>
    <button style="align-self:end">Add</button>
    <button type="button" style="align-self:end" @click="todos.reverse()">Reverse</button>
  </form>
  <ul class="lr-list">
    <template kj-for="todo in todos" :key="todo.id">
      <li :class="{ done: todo.done }">
        <input type="checkbox" kj-model="todo.done" :aria-label="'Done: ' + todo.text">
        <span kj-text="todo.text"></span>
        <button type="button" @click="todos = todos.filter(t => t !== todo)" :aria-label="'Remove ' + todo.text">✕</button>
      </li>
    </template>
  </ul>
  <template kj-if="todos.length === 0"><p class="muted">Nothing left — nice.</p></template>
  <p class="row" style="margin-top:12px"><span class="muted" kj-text="todos.filter(t => t.done).length + ' of ' + todos.length + ' done'"></span>
    <button type="button" @click="confirm = true" :disabled="!todos.some(t => t.done)">Clear done…</button></p>
  <template kj-teleport="body">
    <div class="lr-modal" kj-show="confirm" @keydown.escape.window="confirm = false" role="dialog" aria-modal="true" aria-label="Clear completed items">
      <div><p style="margin-top:0">Remove <b kj-text="todos.filter(t => t.done).length"></b> completed items?</p>
        <div class="row"><button type="button" @click="todos = todos.filter(t => !t.done); confirm = false">Remove</button>
        <button type="button" @click="confirm = false">Cancel</button></div></div>
    </div>
  </template>
</div>

More examples

Index, objects and ranges

HTML
<template kj-for="(user, i) in users" :key="user.id"><li kj-text="(i + 1) + '. ' + user.name"></li></template>
<template kj-for="(value, key) in settings"><dt kj-text="key"></dt></template>
<template kj-for="n in 5"><span>★</span></template>

API

<template kj-if="expr"> · <template kj-for="(item, i) in items" :key="item.id">

Add/remove DOM; keyed lists over arrays, objects or ranges.

Reference: kj-if / kj-for →
kj-model[.lazy .number .trim .boolean .debounce.500ms .throttle .fill]="path"

Two-way binding for inputs, checkboxes (booleans or arrays), radios and selects.

Reference: kj-model →
kj-data="{ … }" · kj-data="component(args)" · init() · destroy()

Declare a component. Nested components see their parents.

Reference: kj-data →

Migrating

Coming from Alpine.jsx-for, x-if and x-teleport work as kj-for, kj-if and kj-teleport — and the x- spellings are accepted too.

Related features