- ClayJS
- Build
- Pre-ship checklist
Pre-ship checklist
The pre-ship checklist and the index of every trap in this guide.
Before you ship a file#
- Loads with
clay.jsas a classic script at the end of<body>, every tag on the same version prefix. <html autosave>unless it is a deliberate manual-save writer.syncis in the plugin list, andexclude=richclayif there is no rich text.- The fallback rule
html:not([editmode="true"]) [show-when\:editmode="true"] { display: none }is in the stylesheet, and every edit-only control hasshow-when:editmode="true". - Records are authored in the saved form (
inert-contenteditable,disabled,readonly); templates in the active form. - Every repeated record has a unique
data-id; templates carry none; every create and duplicate mints a fresh one. - Every control whose value matters has
persist; no filter or search box does. - All chrome is cloned from a
<template>intoclay="editor-ui"afterclay.ready; nothing in the file is chrome. - Tab-local state is in the hash,
localStorageor memory, painted through a runtime style; opening, filtering and searching leaveclay.hasUnsavedChanges()false. - Derived values are sapjs paints, in
editor-ui, orno-dirtywhen the file must show them; every hand-written derived write is guarded, and no script copies a record into a second attribute. - Commands are wrapped in
clay.undo.commit; derived views are drawn inclay.effect, with no effect warnings in the console. - Views whose record a peer deleted are closed, in the effect or a
clay:sync-appliedhandler. - The app script starts with
clay.start; all listeners are delegated. - Nothing writes the record at boot.
- Edit-only calls are guarded for view mode, and edit actions check
clay.isEditMode. clay.confirmcalls have a rejection handler.- Save failures are visible (indicator,
savestatusCSS or clay-ui). - Library residue (Sortable, third-party editors) is stripped in a snapshot hook.
- Images use
clay.upload, embedding only onunsupported. - The file opens in view mode with JavaScript disabled and shows correct content and no edit controls.
- With sap:
Sap.status().ok, the live root matches the file after load (no computed value rewritten), no[sap-error], rows with your own ids.
Testing a document that changes modes#
Each check names the clayjs test that proves the library side.
- It opens in view by default.
tests/unit/mode-decision.test.jspins the table, including an owner cookie alone opening in view with permission kept. - An explicit edit enters edit. The same file covers
?editmode=true,window.clayEditMode = trueand<html opens-in="edit">, andtests/unit/mode-transition.test.jscovers an explicit initial edit preparing the session a live first entry does. - A draft document stays a draft.
tests/unit/mode-draft.test.jsshows a declared draft refusing both directions. - A refused exit can be retried.
tests/unit/mode-lifecycle.test.jscovers a quiesce that refuses and resumes the same leases, andtests/unit/mode-transition.test.jscovers the next entry starting a fresh session. - Pending work refuses the exit.
tests/unit/mode-transition.test.jscovers a registered pending source, andtests/unit/mode-save-barrier.test.jscovers the barrier’s codes. - A clean enter and exit writes nothing.
tests/unit/mode-transition.test.jsasserts it, and the corpus caseenter-leave-zero-writesrecords the same run.
tests/mode-counterexamples/ is a set of recorded cases. Each one boots a fixture in jsdom through the real loader, applies a schedule with the transport, the chunk imports and the clock supplied by the harness, and judges the result from what the run left behind. It is not a browser run: no phone, no keyboard and no real network appears in it.
Trap index#
| Trap | Section |
|---|---|
CSS keyed on editmode="false" shows edit chrome before boot | The root attributes |
A never-saved file shows show-when:editmode controls to viewers without the fallback rule | Showing and hiding edit controls: clay-options |
show-when matches any ancestor, not the nearest | Showing and hiding edit controls: clay-options |
| A view attribute on a saved element syncs to every collaborator | Showing and hiding edit controls: clay-options, Per-tab state |
Records authored with live contenteditable stay editable for viewers until the first save | Mode attributes: one file for editors and viewers |
| Templates in the inert form give clones that cannot be edited until reload | Mode attributes: one file for editors and viewers |
| Unguarded edit-only API calls throw in view mode | What works in view mode |
Reading clay.mode once at boot and keeping it | How the mode is decided |
Using clay.canEdit as the guard for writing; use clay.modeLifecycle.canWrite() | Switching without a reload: clay-mode-switch=“live” |
Switching modes by writing the root editmode attribute | Switching modes: clay.setMode |
hidden, .selected, <details open> and library inline styles save; use ephemeral | What a save writes, Ephemeral state: per-tab state on saved elements |
no-save chrome still reaches peers; use editor-ui | Regions: the clay attribute |
Edits in no-watch or no-dirty are invisible to save() and the close warning | Regions: the clay attribute |
Runtime changes inside freeze are never saved or synced, even by force() | Regions: the clay attribute |
A freeze scratchpad from a peer arrives empty; fill it on every change | Regions: the clay attribute |
Content in a freeze element when it first appears becomes its saved contents | Regions: the clay attribute |
A control without persist loses its typed value | persist: form controls |
Undo and merges restore checked/value attributes but not what the control shows, and the next save writes what is shown | persist: form controls, undo: whole-page undo |
JSON with an unescaped </script> corrupts the file | Where to keep data that is not visible text |
<template> content edits never autosave | Where to keep data that is not visible text |
+= on a live counter loses increments from other tabs; use add | Live data: clay="live" |
| A list in a live store pushed from two tabs keeps one push | Live data: clay="live" |
| A live tag added after people loaded the page is not in their rooms | Live data: clay="live" |
| Live data gets no close warning; up to 8 s can be lost | Live data: clay="live" |
| Items without ids added in two tabs: one is dropped | Element identity: data-id and id, What a merge does |
Cloned records share a data-id, which counts as no id | Element identity: data-id and id |
crypto.randomUUID is missing outside secure contexts | Element identity: data-id and id |
| Writes in the first 0.5 to 3 s after load are absorbed into the baseline and not saved | Autosave |
| A failed save is not retried until the next edit | Autosave |
ok: false also means “nothing to save” and “queued” | The save API |
clay.getHTML() runs hooks and closes the undo batch | The save API |
| A 412 conflict stops autosave until resolved | Conflicts on save (412) |
| Impure hooks keep the page permanently dirty | Hooks |
A throwing onSnapshot or addDocumentTransform callback blocks every save | Hooks |
onaftersave writing to the live DOM loops autosave | Hooks |
| Without autosave, collaborators see nothing until a save | What sync is |
| Listeners attached to records are lost when a merge recreates them | What a merge does |
| A peer’s frame re-runs inline scripts | What a merge does, Starting the app once |
A no-dirty region replaced by a peer raises a false conflict notice (1.8.2) | Regions and JSON under sync |
Same-key JSON edits under merge: incoming wins, yours goes to the notice | Regions and JSON under sync |
| Rewriting the record at boot makes incoming frames hold | Focus, typing and long-running editors |
| Parsed JSON goes stale after a merge | Reconciling in app code |
| A script that copies a record into a styling attribute goes stale in the file, and a merge brings the stale copy back | Rules for DOM state, sap.js: records, views and totals in markup |
An api rule that reads a painted value can read a stale one until a tab repaints and saves | sap.js: records, views and totals in markup |
Full sapjs markup under the sapjs core halts with E50 | sap.js: records, views and totals in markup |
| Presence is runtime only: the host filters the roster, and a caret it cannot place is not drawn | Presence |
data-richclay card mode deletes images on load | richclay: rich text (editable) |
The richclay toolbar is unusable inside showModal() and popovers | richclay: rich text (editable) |
| Rich text has its own undo; page undo never records it | richclay: rich text (editable), undo: whole-page undo |
| Squire undo can roll back a peer’s edit in the same region | richclay: rich text (editable) |
Global registerButton after load does not reach mounted editors | richclay: rich text (editable) |
Screenshot paste does nothing without a pasteImage listener | richclay: rich text (editable) |
| Sortable handle mode is fixed at bind time | sortable: drag to reorder |
onsorting cannot veto a move | sortable: drag to reorder |
| Sortable residue saves | sortable: drag to reorder, Hooks |
| The undo plugin takes Cmd+Z in every text field | undo: whole-page undo |
| Undoing an add removes the record even after a peer edited it | undo: whole-page undo |
Pausing undo across an await loses the earlier mutations | undo: whole-page undo |
| Sap reads live control values: undo without re-syncing inputs saves the undone number | persist: form controls, sap.js: records, views and totals in markup |
clay.upload is not a picker; embed only on unsupported | upload and quickcrop |
clay.confirm rejects with undefined on cancel | clay-ui: toasts and dialogs |
A live element passed to clay.modal.html is deleted | clay-ui: toasts and dialogs |
| Mutation hooks that write what they watch loop forever | clay-events, clay-dom and All |
onclone does not fire for template fragments | clay-events, clay-dom and All |
el.nearest reads a neighbour record’s field | clay-events, clay-dom and All |
hide(), show(), css() write saved inline styles | clay-events, clay-dom and All |
| A file whose painted values do not match sap’s state writes at load | sap.js: records, views and totals in markup |
Dialog prompts are HTML: record text in clay.confirm runs a collaborator’s markup | clay-ui: toasts and dialogs |
editmode:resource type="inert/javascript" activates as javascript and never runs | Mode attributes: one file for editors and viewers |
| Sap’s template row shows before sap loads | sap.js: records, views and totals in markup |
CMS cms-template with hidden makes every new item invisible | cms: content-managed pages |
| Records stored twice drift | The record container |
applyData pairs rows by content and order, not by id | Import and export |
| Activity logged from a mutation observer also logs peers and undo | Commands |
| Saves slow down past about 1,000 records and stall past 3,000 | Document size and record limits |
Updating a runtime style tag without no-dirty can raise the leave warning | Styles a script writes |
| A version restore on hyperclay.com does not reach open editors | What each host gives |
| Hidden is not private | Privacy |
Typed values vanish from a visitor’s draft: the control has no persist | The contract |
Part of the ClayJS complete guideDownload the whole guide as Markdown