# sap

> Build reactive records, filtered views, detail editors, and computed totals in markup with SapJS and ClayJS.

Source: https://clayjs.com/docs/plugins/sap/

`sap.js` is a reactive attribute layer whose only store is the DOM. Every change runs one pass: it reads declared records and fields from the live page, computes `calc:` expressions in dependency order, and updates only the paints that changed. Because the DOM is the source of truth, a [live sync](https://clayjs.com/docs/plugins/live-sync/) merge, an [undo](https://clayjs.com/docs/plugins/undo/) replay, or a hand edit in developer tools feeds directly into the next pass.

## Loading

On any page with a `[sap]` root at boot, ClayJS loads the `derive` plugin (the SapJS core) automatically. The core covers records (`bind`, `state=`, `items`, `item`) and paints (`text`, `text:fmt`, `show`, `attr:`, `class:`, `css:`, and the `show-when:` family).

To use `calc:`, `each`, `detail`, `effect`, `invalid`, action attributes (`trigger-add`, `trigger-remove`, `set:`, `move:`, `sort:`, `confirm`), or `Sap(el)`, load the full `sap.js` [satellite](https://clayjs.com/docs/satellites/) tag:

```html
<script src="https://clayjs.com/v1/clay.js?plugins=sync,sortable,undo,indicator&exclude=richclay"></script>
<script src="https://clayjs.com/v1/sap.js"></script>
```

The markup stays identical, and full `sap.js` takes over from the core. Add `[item][template] { display: none }` to your stylesheet so template rows stay hidden before scripts run.

## Records and binding

Mark the root container with `sap`. Use `items="name"` on a container to make each `[item]` direct child a record in `state.name`, and mark one row `[item][template]` (or place a shared `<template items="name">` anywhere in the root) as the blueprint for new records:

```html
<main id="app" sap state="q:transient open:transient">
  <ol items="cards">
    <li item template data-id="" state="data-due data-done:presence">
      <input type="checkbox" bind="done" persist>
      <span bind="title" editmode:contenteditable contenteditable="plaintext-only"></span>
    </li>
    <li item data-id="c-brief" state="data-due data-done:presence" data-due="2026-10-20">
      <input type="checkbox" bind="done" persist>
      <span bind="title" editmode:contenteditable inert-contenteditable="plaintext-only">Write the brief</span>
    </li>
  </ol>
  <button type="button" trigger-add="cards" show-when:editmode="true">+ Add card</button>
</main>
```

- **`state=`** on a scope (`[sap]`, `[item]`, `[detail]`, or `[scope]`) declares attribute-backed fields. `state="data-due data-done:presence"` reads `data-due` as `item.due` and `data-done` presence as the boolean `item.done`.
- **`bind`** two-way binds a form control or a text leaf inside the scope. Add [`persist`](https://clayjs.com/docs/attributes/) to every bound `<input>`, `<textarea>`, and `<select>` inside a record so typed values write to attributes on save.
- **`data-id`** gives each record a stable identity (`item.$key`) for live sync and views. Put `data-id=""` on the blueprint so `trigger-add` and `Sap(el).$add()` mint a fresh ID on clone.
- **`transient`** (`state="q:transient"` and `transient` on a bound search box) keeps per-tab state in memory only, so search queries and open-record keys never save or sync to peers.

## Lists with each

While `items=` holds the saved records in the file, `each=` renders a reactive view of any array, such as a filtered, sorted, or grouped copy of those records, without modifying or moving the underlying records:

```html
<ul each="state.cards.filter(c => !c.done).sort((a, b) => a.due.localeCompare(b.due))">
  <template>
    <li>
      <input type="checkbox" bind="done">
      <span text="item.title"></span>
    </li>
  </template>
</ul>
```

SapJS clones the container's `<template>` once per array element and places the rows after the `<template>` in array order. Rows are matched by `key=` (which defaults to `item.$key`), so reordering moves existing DOM nodes and preserves focus and scroll position.

Every row generated by `each=` carries [`clay="editor-ui"`](https://clayjs.com/docs/regions/). ClayJS never saves or syncs view rows, and each tab renders its own view. Always filter records with an `each=` view (or a `clay="freeze"` stylesheet) rather than putting `show=` on saved records, because `show=` writes `hidden` into the saved file.

## Edit records in a list

You can edit records straight from an `each=` view row, or open a selected record in a `<dialog>` or side panel with `detail=`.

### Editing straight from an each= row

When an element in an `each=` array is a record from an `items=` list, controls and action attributes inside that view row read from and write straight through to the underlying record:

```html
<nav each="state.notes.filter(n => n.title.toLowerCase().includes(state.q.toLowerCase()))">
  <template>
    <div class="row">
      <input bind="title">
      <button type="button" trigger-remove confirm="Delete this note?">Delete</button>
    </div>
  </template>
</nav>
```

A `bind` or `set:` for a field the record owns edits that record; any other field name resolves against the scope that holds the `each=` container. Inside the row, `trigger-remove` removes the record and `Sap(el)` returns the record's proxy.

### Opening a record in a dialog with detail=

`detail="LIST by KEY"` projects one record into a `<dialog>` or panel. Clicking a record sets a `transient` key with `set:open="item.$key"`, and controls inside the `detail=` element read from and write through to the matched record:

```html
<main id="board" sap state="open:transient">
  <section items="lists">
    <div class="list" item data-id="l-todo">
      <h2 bind="name">To do</h2>
      <ol class="cards" items="cards">
        <li class="card" item data-id="c-brief" state="data-title" data-title="Write the brief" set:open="item.$key">
          <span text="item.title">Write the brief</span>
        </li>
      </ol>
    </div>
  </section>

  <div clay="freeze">
    <dialog detail="lists.cards by state.open">
      <input bind="title">
      <form method="dialog"><button>Close</button></form>
    </dialog>
  </div>
</main>
```

- List paths can walk nested lists: `lists.cards` finds a card inside any list's `cards`.
- On a `<dialog>`, SapJS calls `showModal()` when a record matches the key and `close()` when none does. Closing the dialog with Escape or a `method="dialog"` form clears a plain `state.NAME` key so clicking the same card opens it again.
- Wrap every `detail=` dialog or panel in [`clay="freeze"`](https://clayjs.com/docs/regions/). The dialog's controls are painted with the open record's values, and `showModal()` sets `open`; `clay="freeze"` keeps those runtime changes out of the saved file while preserving the empty dialog element (unlike `clay="editor-ui"`, which would remove the dialog from the saved file).

## Totals and derived paints

Compute derived values with `calc:` and write them into the page with paints (`text`, `text:usd2`, `show`, `attr:`, `class:`, `css:`):

```html
<main id="app" sap>
  <output calc:total="sum(state.txns, 'amount')" text:usd2="state.total">$900.00</output>
  <table>
    <tbody items="txns">
      <tr item template data-id="">
        <td><input type="number" step="0.01" bind="amount" persist></td>
      </tr>
      <tr item data-id="t-k1">
        <td><input type="number" step="0.01" bind="amount" persist value="900"></td>
      </tr>
    </tbody>
  </table>
</main>
```

ClayJS treats every value owned by a paint as derived:

- Painted values are saved into the file so the document renders complete totals before JavaScript runs.
- Comparisons, the dirty check, autosave, undo, and live-sync merges ignore painted output and recompute it from the records after every change or merge. Two tabs editing different rows never conflict on the total.

For more on ownership rules, undo control syncing, and running SapJS in ClayJS, see [sap.js: records, views and totals in markup](https://clayjs.com/docs/guide/components/#sap-js-records-views-and-totals-in-markup).

## Cost at size

Every pass reads the records from the page into fresh objects. To make that cheaper, Sap keeps the raw strings it read from each record's attributes and bound text, and reads an element again only after the page reports a change to it. Sap's own observer clears that copy, so a change the mutation bridge filters or delays still clears it. Parsing runs on every pass, a control's value is always read live, and a full pass (mount, `Sap.refresh()`, a live sync resync) drops every copy. There is no setting and no API for it.

The pass still visits every record and builds fresh objects, so its cost grows with the record count. At 5,000 records in jsdom, a body edit reads 20,091 values instead of 50,085, and its pass takes 26.7 ms instead of 37.8 ms. See [Document size and record limits](https://clayjs.com/docs/guide/architecture/#document-size-and-record-limits).

## Across a mode change

SapJS is not an edit-only layer. The core paints in view as well as in edit, and it has no lease of its own: it reads the mode when it writes, on a document that [switches without a reload](https://clayjs.com/docs/guide/edit-mode/#switching-without-a-reload-clay-mode-switch-live) as well as on one that reloads. A page that never enters edit still gets its totals, its filtered views and its detail panels.

The guard is on the write, not on the paint. A bound control is mirrored into its attribute only while `clay.modeLifecycle.canWrite()` is true, so in view, and while a transition is running, the control's value stays where it is and no record changes. A `transient` value writes without asking, and so does a control inside a region that resolves `persist: "none"`; a page with no mode lifecycle at all writes as it always did.

On `clay:modechange` the core revisits its undo and redo subscriptions: it compares the `clay.undo` member and `clay.modeLifecycle.state.generation` with the pair it last bound, and rebinds only when the generation changed. That is how a redo control keeps working across a mode change, [leaving edit](https://clayjs.com/docs/guide/edit-mode/#leaving-edit) included.
