clay.js
  1. ClayJS
  2. Build
  3. Pre-ship checklist

Pre-ship checklist

The pre-ship checklist and the index of every trap in this guide.

Before you ship a file#

  1. Loads with clay.js as a classic script at the end of <body>, every tag on the same version prefix.
  2. <html autosave> unless it is a deliberate manual-save writer.
  3. sync is in the plugin list, and exclude=richclay if there is no rich text.
  4. The fallback rule html:not([editmode="true"]) [show-when\:editmode="true"] { display: none } is in the stylesheet, and every edit-only control has show-when:editmode="true".
  5. Records are authored in the saved form (inert-contenteditable, disabled, readonly); templates in the active form.
  6. Every repeated record has a unique data-id; templates carry none; every create and duplicate mints a fresh one.
  7. Every control whose value matters has persist; no filter or search box does.
  8. All chrome is cloned from a <template> into clay="editor-ui" after clay.ready; nothing in the file is chrome.
  9. Tab-local state is in the hash, localStorage or memory, painted through a runtime style; opening, filtering and searching leave clay.hasUnsavedChanges() false.
  10. Derived values are sapjs paints, in editor-ui, or no-dirty when the file must show them; every hand-written derived write is guarded, and no script copies a record into a second attribute.
  11. Commands are wrapped in clay.undo.commit; derived views are drawn in clay.effect, with no effect warnings in the console.
  12. Views whose record a peer deleted are closed, in the effect or a clay:sync-applied handler.
  13. The app script starts with clay.start; all listeners are delegated.
  14. Nothing writes the record at boot.
  15. Edit-only calls are guarded for view mode, and edit actions check clay.isEditMode.
  16. clay.confirm calls have a rejection handler.
  17. Save failures are visible (indicator, savestatus CSS or clay-ui).
  18. Library residue (Sortable, third-party editors) is stripped in a snapshot hook.
  19. Images use clay.upload, embedding only on unsupported.
  20. The file opens in view mode with JavaScript disabled and shows correct content and no edit controls.
  21. 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.js pins 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 = true and <html opens-in="edit">, and tests/unit/mode-transition.test.js covers an explicit initial edit preparing the session a live first entry does.
  • A draft document stays a draft. tests/unit/mode-draft.test.js shows a declared draft refusing both directions.
  • A refused exit can be retried. tests/unit/mode-lifecycle.test.js covers a quiesce that refuses and resumes the same leases, and tests/unit/mode-transition.test.js covers the next entry starting a fresh session.
  • Pending work refuses the exit. tests/unit/mode-transition.test.js covers a registered pending source, and tests/unit/mode-save-barrier.test.js covers the barrier’s codes.
  • A clean enter and exit writes nothing. tests/unit/mode-transition.test.js asserts it, and the corpus case enter-leave-zero-writes records 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#

TrapSection
CSS keyed on editmode="false" shows edit chrome before bootThe root attributes
A never-saved file shows show-when:editmode controls to viewers without the fallback ruleShowing and hiding edit controls: clay-options
show-when matches any ancestor, not the nearestShowing and hiding edit controls: clay-options
A view attribute on a saved element syncs to every collaboratorShowing and hiding edit controls: clay-options, Per-tab state
Records authored with live contenteditable stay editable for viewers until the first saveMode attributes: one file for editors and viewers
Templates in the inert form give clones that cannot be edited until reloadMode attributes: one file for editors and viewers
Unguarded edit-only API calls throw in view modeWhat works in view mode
Reading clay.mode once at boot and keeping itHow 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 attributeSwitching modes: clay.setMode
hidden, .selected, <details open> and library inline styles save; use ephemeralWhat a save writes, Ephemeral state: per-tab state on saved elements
no-save chrome still reaches peers; use editor-uiRegions: the clay attribute
Edits in no-watch or no-dirty are invisible to save() and the close warningRegions: 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 changeRegions: the clay attribute
Content in a freeze element when it first appears becomes its saved contentsRegions: the clay attribute
A control without persist loses its typed valuepersist: form controls
Undo and merges restore checked/value attributes but not what the control shows, and the next save writes what is shownpersist: form controls, undo: whole-page undo
JSON with an unescaped </script> corrupts the fileWhere to keep data that is not visible text
<template> content edits never autosaveWhere to keep data that is not visible text
+= on a live counter loses increments from other tabs; use addLive data: clay="live"
A list in a live store pushed from two tabs keeps one pushLive data: clay="live"
A live tag added after people loaded the page is not in their roomsLive data: clay="live"
Live data gets no close warning; up to 8 s can be lostLive data: clay="live"
Items without ids added in two tabs: one is droppedElement identity: data-id and id, What a merge does
Cloned records share a data-id, which counts as no idElement identity: data-id and id
crypto.randomUUID is missing outside secure contextsElement identity: data-id and id
Writes in the first 0.5 to 3 s after load are absorbed into the baseline and not savedAutosave
A failed save is not retried until the next editAutosave
ok: false also means “nothing to save” and “queued”The save API
clay.getHTML() runs hooks and closes the undo batchThe save API
A 412 conflict stops autosave until resolvedConflicts on save (412)
Impure hooks keep the page permanently dirtyHooks
A throwing onSnapshot or addDocumentTransform callback blocks every saveHooks
onaftersave writing to the live DOM loops autosaveHooks
Without autosave, collaborators see nothing until a saveWhat sync is
Listeners attached to records are lost when a merge recreates themWhat a merge does
A peer’s frame re-runs inline scriptsWhat 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 noticeRegions and JSON under sync
Rewriting the record at boot makes incoming frames holdFocus, typing and long-running editors
Parsed JSON goes stale after a mergeReconciling 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 backRules 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 savessap.js: records, views and totals in markup
Full sapjs markup under the sapjs core halts with E50sap.js: records, views and totals in markup
Presence is runtime only: the host filters the roster, and a caret it cannot place is not drawnPresence
data-richclay card mode deletes images on loadrichclay: rich text (editable)
The richclay toolbar is unusable inside showModal() and popoversrichclay: rich text (editable)
Rich text has its own undo; page undo never records itrichclay: rich text (editable), undo: whole-page undo
Squire undo can roll back a peer’s edit in the same regionrichclay: rich text (editable)
Global registerButton after load does not reach mounted editorsrichclay: rich text (editable)
Screenshot paste does nothing without a pasteImage listenerrichclay: rich text (editable)
Sortable handle mode is fixed at bind timesortable: drag to reorder
onsorting cannot veto a movesortable: drag to reorder
Sortable residue savessortable: drag to reorder, Hooks
The undo plugin takes Cmd+Z in every text fieldundo: whole-page undo
Undoing an add removes the record even after a peer edited itundo: whole-page undo
Pausing undo across an await loses the earlier mutationsundo: whole-page undo
Sap reads live control values: undo without re-syncing inputs saves the undone numberpersist: form controls, sap.js: records, views and totals in markup
clay.upload is not a picker; embed only on unsupportedupload and quickcrop
clay.confirm rejects with undefined on cancelclay-ui: toasts and dialogs
A live element passed to clay.modal.html is deletedclay-ui: toasts and dialogs
Mutation hooks that write what they watch loop foreverclay-events, clay-dom and All
onclone does not fire for template fragmentsclay-events, clay-dom and All
el.nearest reads a neighbour record’s fieldclay-events, clay-dom and All
hide(), show(), css() write saved inline stylesclay-events, clay-dom and All
A file whose painted values do not match sap’s state writes at loadsap.js: records, views and totals in markup
Dialog prompts are HTML: record text in clay.confirm runs a collaborator’s markupclay-ui: toasts and dialogs
editmode:resource type="inert/javascript" activates as javascript and never runsMode attributes: one file for editors and viewers
Sap’s template row shows before sap loadssap.js: records, views and totals in markup
CMS cms-template with hidden makes every new item invisiblecms: content-managed pages
Records stored twice driftThe record container
applyData pairs rows by content and order, not by idImport and export
Activity logged from a mutation observer also logs peers and undoCommands
Saves slow down past about 1,000 records and stall past 3,000Document size and record limits
Updating a runtime style tag without no-dirty can raise the leave warningStyles a script writes
A version restore on hyperclay.com does not reach open editorsWhat each host gives
Hidden is not privatePrivacy
Typed values vanish from a visitor’s draft: the control has no persistThe contract
Part of the ClayJS complete guideDownload the whole guide as Markdown