Some state changes in the browser without a round trip, and outlasts a page load. Where the browser keeps it decides what the server knows of it, so each store has a namespace of its own:
co.multiply.tropical.cookie | co.multiply.tropical.storage | |
|---|---|---|
| For | whether a section is open, a sidebar folded | a draft the user is writing |
| Kept in | a cookie | localStorage |
| The server sees it | as the session begins, so it renders the first frame | when an action passes it, or it reads it (observe) |
| Values | booleans, numbers and strings, about 3.5 KB in all | JSON, of any size |
| Sent | with every request | only as an action's value |
| Per user | no | yes |
| Dropped | the oldest first, past the cookie's size | 30 days after it was last written, by default |
Both keep a value in a Datastar signal, local to the page, which an island declares on its root, only if missing, so a
later render, a patch or the island mounting again never undoes what the user did. Both give a handle for the helpers
in co.multiply.tropical.ui. The element the app mounts in, (ring/mount :body attrs), carries ui/page-attrs,
whose handler writes each change into the store (Serving pages).
(defcookie group-open
"Whether a sidebar group is open, per group."
{:default true :valid? boolean?})
(defisland sidebar-group
{:key :id}
[group]
(let [open (use-cookie group-open (:id group))]
[:section
[:button (ui/aria-expanded {:data-on:click (ui/toggle open)} open) (:name group)]
[:ul (ui/shown open) ...]]))
defcookie declares it, with a default and what a value must be, wherever the code using
it lives: next to one island, or in a namespace several share. Any island in the page takes it up with use-cookie,
with an item, such as a group's id, for one per item. Islands using one state follow one signal.ui/shown and ui/aria-expanded render the plain attribute from it, and bind it to the signal; ui/signal
gives the signal for expressions of your own, such as a pane's width in data-style. cookie/value is the session's
value, for the first paint only: the browser's may have moved on, so bind what must follow it.(cookie/patch [[group-open id false]]), merged with its own signals: the
islands using the state follow, and the cookie keeps it, for an item the page doesn't render too. For the page it
opens, such as a new item's with a choice made before the item existed, it passes the same signals to navigate:
(action/navigate (str "/groups/" id) {:signals (cookie/patch [[group-open id false]])}) has the cookie written
before the page loads, so its first frame has them. Values are checked as the handler builds its result, and an
invalid one throws.(defstorage edits
"Edits to a suggestion, before the user acts on it."
{:default {} :valid? map?})
(defisland rework
[{:keys [id text]}]
(let [draft (use-storage edits id {:default {"text" text}})
start (use-action :start
(fn [_ value]
(when-some [{t "text"} (storage/decode edits value)]
(start! id t))
(storage/forget [[edits id]]))
{:value (ui/signal draft)})]
[:form
[:textarea {:data-bind (ui/path draft "text")}]
[:button {:type "button" :data-on:click start} "Start"]]))
start does, and
storage/decode reads it back: through the state's :decode, and nil unless :valid?, since it is untrusted.use-storage, with an item for one per item, declares the signal from the
browser's entry, or from the default if there is none. A default of the island's own, {:default v}, is the item's
starting point, such as the suggestion's text, and the browser keeps an entry only while the value differs from it:
only edits are stored, and a default the server revises shows through where the user changed nothing.data-bind takes ui/path, a path into the value,
and expressions take ui/signal.(storage/observe draft) returns pending, then the browser's value,
decoded, and the latest each time the user stops typing, while the island reads it: for what only the server can
render, such as a row per item, or a count. The browser sends it once the page has the island, so the first frame
never has it, and doesn't wait for it.:encode
makes one from a Clojure value, and :decode reads one back; defstorage checks that its default comes back as it
went, so a lossy mapping, such as keywords without a :decode, fails as it is declared. A value only ever
round-tripped can be a string, such as Transit, at the cost of binding to it.:max-age-ms, stored under another :version (change it
when the values' shape changes), or of a state no longer declared. When the storage is full, the oldest go first.(storage/patch [[edits id v]]) or (storage/forget [[edits id]]), merged with its own signals, and the browser that made the action keeps or drops the entries, whether or not
the page uses them. An island using a forgotten entry goes back to the default it declared. An action that opens
another page passes them to navigate's :signals, so a draft the action used up is gone before that page loads:
(action/navigate (str "/projects/" id) {:signals (storage/forget [[edits suggestion-id]])}).Any attribute, over any expression. (ui/attr attrs k value expr) is what shown and aria-expanded are built
on: the attribute k follows the Datastar expression expr, from a first paint showing value, the server's
reckoning of it, and stays as the browser has it when the island renders again. An aria-* state gets the words
true and false, where Datastar would leave a boolean's true empty, which counts as no state. Thread several onto
one element, rather than merge them, so each stays preserved:
[:button (-> {:role "tab" :data-on:click "$tab = 'files'"}
(ui/attr :aria-selected (= tab :files) "$tab === 'files'")
(ui/attr :tabindex (if (= tab :files) 0 -1) "$tab === 'files' ? 0 : -1"))
"Files"]
[:button (ui/attr {:type "submit"} :disabled (= draft saved) "$draft === $saved") "Save"]
Text that follows an expression needs no helper: [:span {:data-text expr} first-paint-text], and Datastar puts the
browser's text back when a render changes it, before the page paints.
A handle's signal. ui/signal names it in an expression, ui/path in a data-bind, and ui/toggle flips a
boolean one.
A value of the server's, in an expression. ui/json writes it as a JavaScript literal, (str "$tab = " (ui/json tab)), where a value spliced in as it is written could become code (Security).
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |