- Buy milk
- Call the landlord
For a small app that lives in one HTML file and keeps its data as elements in that file. Stores all its state in the DOM.
<main sap id="hero-demo">
sap marks the app. sapjs reads everything inside it again after each change. <ul items="todos">
items names a list. Each child marked item is one task. <li item template>
template is the blank task that every new one is copied from. <input type="checkbox" bind="done" persist>
bind makes the checkbox a field of the task. persist writes its checked state into the markup, so saving the file keeps it. <span bind="title"></span>
</li>
<li item><input type="checkbox" bind="done" persist checked> <span bind="title">Buy milk</span></li>
<li item><input type="checkbox" bind="done" persist> <span bind="title">Call the landlord</span></li>
</ul>
<form trigger-add="todos">
trigger-add copies the template into the list when the form is submitted, fills in the title you typed, and clears the form. <input bind="title" placeholder="Add a task" required>
</form>
<output text="plural(count(state.todos, t => !t.done), 'task', 'tasks') + ' left'">1 task left</output>
text holds a formula. sapjs reruns it after every change and writes the result inside the element, where it saves with the file.</main>
Use sapjs when your whole app lives in one HTML file, different parts of the page need to react to each other and you want all the state stored in the document itself.
bind, items, calc: and text instead of writing render code, and any edit updates the rest of the page.trigger-add, each=, detail= and transient handle adding rows from a form, sorted and filtered views, detail panels and search text that never saves.Say you keep a reading log as one HTML file. Each book is a <tr> in a table. The day you started and the day you finished are attributes on that row. An <output> under the table shows how many books you finished. Open the file in a text editor and every book is there as markup.
The count is a formula in an attribute on that <output>. Add a book or change a date, and sapjs re-reads the page and updates the count. Change a date by hand in devtools and the count updates, the same as after a click. A changed book is read from its row again, and an unchanged one keeps exactly what its row says, so there is no JavaScript copy to drift from the page. How it works shows each step of that re-read.
The last count is written inside the <output> in the file, so it shows before any script runs.
I think sapjs works best when the interface and the data both fit in that one file. You edit the file and share it like any document. Add a finished date to a row in a text editor, open the file, and the count goes up by one. Send the file to a friend and they get the app with every book in it.
ClayJS is the library that saves an HTML file back to its host and syncs it between tabs. Saving takes ClayJS, or another client like it, on a host that accepts saves. Put persist on each input, textarea and select in a record, so what you type reaches the saved markup.
The apps that fit are lists of records. An expense tracker is rows of expenses and a total. A board is columns of cards. With sapjs, each record is an element in the file, and each number is a formula in an attribute.
A form adds an expense as a new row. A calc: attribute sums the amounts, and text:usd2 paints the total as $54.50.
trigger-add · calc: · text:usd2
Columns hold cards as nested lists. Drag a card to another column with ClayJS sortable, and the move saves as the new order of the markup. Click a card and a <dialog> opens to edit it.
items= · detail=
The notes are saved records. The list you see is an each= view, sorted by title and filtered by a search box. The search box carries transient, an attribute that keeps the search text out of the saved file. A detail= panel edits the open note.
each= · transient · detail=
Each book stores the day you started and the day you finished. days() shows how long each one took, and count() shows how many you finished.
calc: · days() · count()
Click a name and a detail= form edits that contact. A sort: button sorts the list by any field, and confirm= asks before a delete.
detail= · sort: · confirm=
Each line computes its own total. The invoice sums the lines and paints $1,234.50. move:up and move:down reorder the lines.
calc: · sum() · move:
If you want running code to read, the guide walks through an expense tracker, a Kanban board and a notes app, and each one runs in the sapjs test suite.
At 10,000 records a change still takes under 40 ms in Chrome. The cost is the file itself: every row is saved markup, so saving takes about 400 ms and a list of 10,000 text boxes takes over a second to lay out. Past that, measure before you build on it.
The data is in the markup. Anyone with the file has all of it.
If the records live behind an API, keeping them in the DOM gives you nothing back.
sapjs state is plain text. A rich-text editor goes inside an element with sap-ignore, an attribute that marks a region sapjs does not read. An app built around such an editor ends up mostly editor code.
On its own, the app runs and recomputes. Nothing saves.
<script src="https://cdn.jsdelivr.net/npm/@panphora/sapjs/dist/sap.min.js"></script>
With ClayJS, the file saves itself on a host that accepts saves. Load sap.js after clay.js, and see the guide for the rest.
<html autosave>
<script src="https://clayjs.com/v1/clay.js?plugins=sync,sortable,undo"></script>
<script src="https://clayjs.com/v1/sap.js"></script>
sapjs by David Miranda. MIT-0 license.