<!-- Visual guide · clayjs · https://clayjs.com/visual-guide · generated from the page; every page has one at its path plus .md -->

[Guide](https://clayjs.com/guide) · Visual

# Malleable documents, drawn

You save and reopen, and an "Add item" button appears twice. Or the next save writes over an agent's edit. Both happen when the saved file and the open page drift apart. [Section 6](https://clayjs.com/visual-guide#minimal) is the smallest complete file: a task list that keeps its edits and order when you reopen it. This page assumes you have loaded ClayJS and saved through a host. If you haven't, start with [Get started](https://clayjs.com/get-started). The attribute reference and the APIs are in the [complete guide](https://clayjs.com/complete-guide).

## 1. One file, two forms

A malleable document exists in two forms. The saved file is the bytes on disk. The open page is the live DOM in an editor's browser. ClayJS turns the file into the page on open, and the page back into a file on save. With the `sync` plugin, it also merges outside changes into the open page.

_Diagram: An agent edits the saved file on disk. ClayJS turns that file into the open page on open, merges outside changes in with the sync plugin, and turns the page back into the file on save._

durable record: saved, synced, seen by viewers · temporary: never saved, rebuilt on every load

Everything green exists in both forms. Everything amber exists only in the open page.

ClayJS handles two sets of marked regions:

-   Stripped regions are `clay="editor-ui"`, `clay="no-save"` and `clay="no-snapshot"`. Saving removes them.
-   Excluded regions are those three plus `clay="freeze"`. Sync skips them.

A `clay="freeze"` region in the file is saved with the contents it had when the page loaded.

ClayJS translates every difference between the two forms that it knows about, in both directions. So keep the two forms as close as the app allows, and mark every difference your code adds.

## 2. How much the two forms differ

Pick the lowest of these four rungs that fits the file. Each rung up moves the two forms further apart and costs you more rules. Gray marks a control saved in the file.

_Diagram: Rung 1: the open page and the saved file are identical, controls included._

### Rung 1. Editors only

The editing UI is the app. Nobody views it anonymously, so nothing is stripped, and controls are ordinary markup in the file. The two forms are the same, apart from state ClayJS adds on open and removes on save, such as `editmode="true"` on `<html>`. So sync has nothing of yours to translate.

_Diagram: Rung 2: the same bytes; a viewer sees the same content with the controls hidden by mode._

### Rung 2. Editors and viewers

Controls stay in the file, each marked `show-when:editmode="true"`. Load `clay-options.js` to make that attribute work. ClayJS sets `editmode="true"` on `<html>` for editors and strips that attribute from the saved file. [Section 6](https://clayjs.com/visual-guide#minimal) shows how to hide them before that script runs.

Editors and viewers load the same bytes. Rung 2 adds one difference: editing attributes such as `inert-contenteditable`, which sync translates for you. The cost is that viewers download the control markup.

_Diagram: Rung 3: the open page has its controls in an editor-ui region; the saved file holds only the content, with inert editing attributes._

### Rung 3. Stripped controls

Controls are created at load inside `clay="editor-ui"`, and fields use `editable` or `editmode:contenteditable`. The saved file is clean viewer HTML.

Sync skips the marked controls. You recreate them on every load and bind events by delegation.

_Diagram: Rung 4: the open page shows a chart drawn by JavaScript inside an editor-ui mount; the saved file holds only the JSON record._

### Rung 4. Rendered views

The durable record is data: markup, or a `merge="store"` JSON script. A canvas, chart or grid is drawn from it into a mount created at runtime inside `clay="editor-ui"`. The saved file holds only the record.

A `clay="freeze"` mount also works. Author it empty in the file, and draw into it only after `clay.ready`. Drawing before `clay.ready` freezes the drawing into the file.

Sync merges the record and never touches the view. Write one render function and call it on load, after every local command, and on `clay:sync-applied`. ClayJS fires that event after it applies an outside change.

## 3. Why stripping makes sync harder

An agent edits the file on disk, so its edit arrives in saved form. Its editing attributes are inert, and from rung 3 up it has no toolbar. ClayJS brings the edit into the open page in three steps.

_Diagram: ClayJS merges the agent's saved-form edit with the open page captured in saved form, activates the editing attributes in the result, then applies it to the open page while skipping the toolbar. A class added by unmarked boot code is not translated and gets saved._

Three cases need care.

-   An outside change never updates an excluded region, so rebuild anything inside one yourself.
-   An unmarked class or button from boot code is ordinary content. The next save writes it, and an outside change can remove it.
-   A viewer's page with `sync` receives the stripped file, never an editor's open page.

## 4. The mistakes, drawn

Each of these breaks saving, syncing or privacy without an error.

_Diagram: Each time the file opens, another copy of the injected button is saved._

### 1. Injected controls without a mark

A script appends an "Add item" button at load. The button goes into the file with the next save, and the next open adds another.

Put `clay="editor-ui"` on the root of any control your code creates at load, or author the control in the file and hide it by mode (rung 2). Then delete the copies already saved.

_Diagram: A note in a no-save region never reaches the file; another editor's page receives it and ignores it._

### 2. Content inside a stripped region

You put editor notes in `clay="no-save"` so viewers won't see them. The notes never reach disk, and they vanish on reload. Another editor's open page receives them and ignores them.

Keep notes that editors share in the file, and hide them by mode. A stripped region holds only what you can rebuild.

_Diagram: An agent writes Blue into the file, the boot script sets Red, and the next save writes Red over the agent's edit._

### 3. Boot code that writes defaults

Boot code fills fields from hardcoded values or a JS object as the page opens. The next save writes those defaults over what an agent or another tab put in the file.

Read values from the DOM at `clay.ready`, and write a default only where the DOM holds none.

_Diagram: Three list items have click handlers; a fourth item that arrived by sync has none._

### 4. Handlers bound once, per element

`querySelectorAll('li').forEach(li => li.onclick = …)` at load. An item that arrives later by sync, or a node the merge replaced, has no handler.

Delegate: one listener on `document` that checks `event.target.closest(…)`.

_Diagram: The record changes from 5 to 9 by sync, but the chart still shows 5._

### 5. Views that do not re-render

A chart, canvas or parsed JSON object is built once at load. Sync updates the record but not the view.

Write one render function, as in rung 4, and call it on `clay:sync-applied` too. Write derived text only when it changed. Writing the same text leaves the file unchanged, but with the undo plugin it still adds an undo step outside `clay="editor-ui"`.

_Diagram: Ids regenerated on every load: after an outside change, the edited task appears twice, as your edit with the page's id and as the file's copy with the file's id._

### 6. Identities that change

Boot code generates a new `id` for each item on every load. The open page and the file then name the same item differently. When an outside change arrives while the page has unsaved edits, every item you edited shows up twice: your edited copy and the file's copy.

Mint an `id` or `data-id` once, when the item is created. Keep it through edits and moves.

_Diagram: A dialog sets overflow hidden on body; saving while it is open writes that style into the file._

### 7. Temporary state on durable elements

A dialog, drag or hover sets a style or class on `<body>` or a content node for a moment. ClayJS's crop dialog and `clay.confirm` set `overflow: hidden` on `<body>` while they are open, so a save in that moment writes it into the file.

Keep transient state in JS or an excluded region. To keep such a style out of every save, remove it in `clay.onSnapshot(fn)`. `fn` receives a copy of the `<html>` element before each save or send. Change that copy, not `document`. For the dialog's `overflow`:

```javascript
clay.onSnapshot((root) => {
  const body = root.querySelector("body");
  if (body.style.overflow === "hidden") body.style.removeProperty("overflow");
});
```

The `hidden` check keeps any other `overflow` value on `<body>`. If your page sets `overflow: hidden` itself, this removes it too.

_Diagram: Content hidden with CSS still travels to the viewer and shows in view source._

### 8. Hiding treated as privacy

CSS, view mode, `viewmode:disabled` and hidden inputs change presentation. Anyone who receives the file receives every byte. Editors' tabs also send each other `no-save` content.

Secret data goes in a separate file behind its own access.

## 5. Where each piece of state goes

Ask these questions of every element, attribute and variable.

_Diagram: Decision flow: temporary state goes in a JS variable or an editor-ui region; derived state is re-rendered into editor-ui; content viewers see is plain markup; clutter stays in the file, hidden by mode; secrets go in a separate file._

If the file itself must show a derived value, render it into a `clay="no-dirty"` region. Writing there starts no save. The value reaches the file with the next save of other content, and an outside change replaces it.

## 6. The smallest rung 2 file

This file is a task list you can add to, rename and reorder. Its controls are in the file and hidden from viewers, which makes it rung 2.

```html
<!DOCTYPE html>
<html lang="en" autosave>
<head>
  <meta charset="utf-8">
  <title>Tasks</title>
  <style>html:not([editmode="true"]) [show-when\:editmode="true"] { display: none }</style>
</head>
<body>
  <h1 editmode:contenteditable inert-contenteditable="plaintext-only">Tasks</h1>
  <ul id="tasks" sortable="tasks">
    <li data-id="t-1"><span show-when:editmode="true" sortable-handle>↕</span> <span editmode:contenteditable inert-contenteditable="plaintext-only">Write the brief</span></li>
  </ul>
  <button show-when:editmode="true" type="button" data-action="add">Add task</button>
  <template id="task-tpl">
    <li><span show-when:editmode="true" sortable-handle>↕</span> <span editmode:contenteditable contenteditable="plaintext-only">New task</span></li>
  </template>

  <script src="https://clayjs.com/v1/clay.js?plugins=sync,sortable,indicator&exclude=richclay"></script>
  <script src="https://clayjs.com/v1/clay-options.js"></script>
  <script>
    document.addEventListener("click", (event) => {
      if (!event.target.closest('[data-action="add"]') || !clay.isEditMode) return;
      const li = document.getElementById("task-tpl").content.firstElementChild.cloneNode(true);
      li.dataset.id = "t-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
      document.getElementById("tasks").append(li);
    });
  </script>
</body>
</html>
```

Open the file through your host. Add a task, rename it, drag it above "Write the brief", and wait for the indicator to show saved. Reopen it: there is still one Add task button, and the task keeps its new name and place.

-   The record is written in its saved form (`inert-contenteditable`), so a viewer gets plain text from the first open. The `<template>` holds the form the open page uses (`contenteditable`), because a clone is only ever inserted while someone is editing.
-   New items come from the template, never from a copy of an item on the page. Each one gets a fresh `data-id`, built without `crypto.randomUUID`, which is missing outside a secure context (for example plain http on a public host).
-   The click handler checks `clay.isEditMode`, because hidden buttons are still in a viewer's page.
-   `sortable="tasks"` makes the list draggable by each item's ↕ grip, marked `sortable-handle`.
-   `exclude=richclay` skips the rich-text editor, because every field here is plain text. `indicator` shows save status.
-   `clay-options.js` turns each distinct `show-when:` attribute and value into one CSS rule, in a `<style data-name="option-visibility">` tag. A file gets that tag with its first save.
-   Without a fallback, a viewer's browser shows the controls of a never-saved file until the script runs. So this file carries one in its own `<style>` tag. The backslash in `show-when\:editmode` escapes the colon. Without it, the browser drops the whole rule. The rule is keyed on `:not([editmode="true"])`, because the saved file has no `editmode` attribute.

Move to rung 3 only when viewers must receive HTML without the controls. For undo, a filter and a computed summary, study [examples/skeleton.html](https://clayjs.com/examples/skeleton.html). It renders the filter and summary into `clay="editor-ui"` and keeps its controls in the file, hidden by mode.

Full reference: the [complete guide](https://clayjs.com/complete-guide).
