Builds the client-side expressions that invoke :on handlers.
1, which recorded this as "unresolved, and currently unsound". This namespace resolves it. The problem was not ergonomics — it was that the hand-written form was wrong:
;; before
[:button {:data-on:click (str "@post('/act?event=remove&id=" (:id i) "')")}]
Three defects, all verified against a real render rather than reasoned about:
' closes datastar's string literal early — o'brien became
@post('...id=o'brien'), a syntax error. An arg containing & split into two
query params. = and + corrupted the parse.(parse-long (get params "id")), hardcoded. A string arg silently became
nil; a boolean could not round-trip at all.Datastar's fetch actions take a payload option that replaces the signal set
and is sent as the request body:
@post('/live/act', {payload: {"event":"remove","id":42}})
This kills all three defects structurally rather than by escaping more carefully:
' and & are unremarkable. The
nested-quoting problem does not arise, because there is no nested quoting.42, "42", true and null stay distinguishable and
the server never guesses.Escaping does not disappear entirely — it moves somewhere tractable. The JSON is
embedded in an HTML attribute inside a JS expression, so a ' or \ within a
string value still has to be escaped for the JS literal. That is what
write-json handles, and it is the one case worth testing hardest, since "just
use JSON" is only 90% of the answer.
liveId has to be merged inNot anticipated by payload replaces the signals datastar would
otherwise send, and liveId is a server-pushed signal — so a bare payload
drops it and every action fails with "no live context". The expression therefore
merges the live-id signal back in explicitly with datastar's $ syntax.
The dispatch path belongs to whatever mounted the route,
so hardcoding /live/act here would reintroduce exactly the drift removed
for element ids. Callers pass it; adapters supply it from their own config.
Whether :on handlers should declare arg schemas is deliberately unsettled.
They do not, for now. Types survive the wire on their own once args are JSON, which was the
actual complaint. Adding an optional :args predicate later is additive — it
slots in front of the handler call without changing this wire format or any
existing handler — so the cheap version is not a dead end. Tracked in
Dependency-free, like the rest of the core: the JSON writer below is a few
lines rather than a reason to put charred on the core's classpath. It follows
snapshot, which hand-rolls its own encoding for the same reason.
Builds the client-side expressions that invoke `:on` handlers.
1, which recorded this as "unresolved, and currently unsound".
This namespace resolves it. The problem was not ergonomics — it was that the
hand-written form was *wrong*:
;; before
[:button {:data-on:click (str "@post('/act?event=remove&id=" (:id i) "')")}]
Three defects, all verified against a real render rather than reasoned about:
1. **Escaping.** Chassis HTML-escapes the attribute, so the markup is valid, but
the browser un-escapes it before the expression is parsed. An arg containing
`'` closes datastar's string literal early — `o'brien` became
`@post('...id=o'brien')`, a syntax error. An arg containing `&` split into two
query params. `=` and `+` corrupted the parse.
2. **Args were int-only by accident.** The receiving handler did
`(parse-long (get params "id"))`, hardcoded. A string arg silently became
nil; a boolean could not round-trip at all.
3. **Types were lost.** Query strings carry only strings, so every handler
needed per-arg coercion it never declared.
## The fix: a JSON payload, not a query string
Datastar's fetch actions take a `payload` option that **replaces** the signal set
and is sent as the request body:
@post('/live/act', {payload: {"event":"remove","id":42}})
This kills all three defects structurally rather than by escaping more
carefully:
- The arg sits inside a **JSON string**, where `'` and `&` are unremarkable. The
nested-quoting problem does not arise, because there is no nested quoting.
- JSON has types, so `42`, `"42"`, `true` and `null` stay distinguishable and
the server never guesses.
Escaping does not disappear entirely — it moves somewhere tractable. The JSON is
embedded in an HTML attribute inside a JS expression, so a `'` or `\` *within a
string value* still has to be escaped for the JS literal. That is what
`write-json` handles, and it is the one case worth testing hardest, since "just
use JSON" is only 90% of the answer.
## `liveId` has to be merged in
Not anticipated by `payload` **replaces** the signals datastar would
otherwise send, and `liveId` is a server-pushed *signal* — so a bare payload
drops it and every action fails with "no live context". The expression therefore
merges the live-id signal back in explicitly with datastar's `$` syntax.
## Why the path is a parameter
The dispatch path belongs to whatever mounted the route,
so hardcoding `/live/act` here would reintroduce exactly the drift removed
for element ids. Callers pass it; adapters supply it from their own config.
## No arg schemas, deliberately
Whether `:on` handlers should declare arg schemas is deliberately unsettled.
They do not, for now. Types survive the wire on their own once args are JSON, which was the
actual complaint. Adding an optional `:args` predicate later is additive — it
slots in front of the handler call without changing this wire format or any
existing handler — so the cheap version is not a dead end. Tracked in
Dependency-free, like the rest of the core: the JSON writer below is a few
lines rather than a reason to put charred on the core's classpath. It follows
`snapshot`, which hand-rolls its own encoding for the same reason.(action method path event args {:keys [signal]})Builds a datastar expression invoking event (with optional args) at path
over method.
Prefer the method-named wrappers — post, get, put, patch, delete — which
is where callers should normally enter. This is the general form they share.
args keys must not collide with "event" or the live-id signal name; that
throws rather than silently overwriting the dispatch target, since an arg named
event would otherwise redirect the action to a different handler.
Options:
:signal the live-id signal name, default "liveId"The live id is interpolated as $liveId rather than baked in, because payload
replaces the signal set that would normally carry it (see the namespace
docstring) and the id is not known at render time on a reconnect.
Builds a datastar expression invoking `event` (with optional `args`) at `path` over `method`. Prefer the method-named wrappers — `post`, `get`, `put`, `patch`, `delete` — which is where callers should normally enter. This is the general form they share. `args` keys must not collide with `"event"` or the live-id signal name; that throws rather than silently overwriting the dispatch target, since an arg named `event` would otherwise redirect the action to a different handler. Options: - `:signal` the live-id signal name, default `"liveId"` The live id is interpolated as `$liveId` rather than baked in, because `payload` replaces the signal set that would normally carry it (see the namespace docstring) and the id is not known at render time on a reconnect.
(delete path event)(delete path event args)(delete path event args opts)A @delete action expression. Sends args as a datastar query parameter rather
than a body — see the section comment above. See action.
A `@delete` action expression. Sends args as a `datastar` query parameter rather than a body — see the section comment above. See `action`.
(get path event)(get path event args)(get path event args opts)A @get action expression. Sends args as a datastar query parameter rather
than a body — see the section comment above. See action.
A `@get` action expression. Sends args as a `datastar` query parameter rather than a body — see the section comment above. See `action`.
(patch path event)(patch path event args)(patch path event args opts)A @patch action expression. See action.
A `@patch` action expression. See `action`.
(post path event)(post path event args)(post path event args opts)A @post action expression. See action.
A `@post` action expression. See `action`.
(put path event)(put path event args)(put path event args opts)A @put action expression. See action.
A `@put` action expression. See `action`.
(raw expr)Marks expr as a JavaScript expression to be emitted unescaped.
For args whose value must be computed on the client, such as
(raw "evt.target.value"). Everything else should be plain data and go through
the normal escaping — raw is an escape hatch, and anything interpolated into it
is the caller's responsibility. Never pass user or database content to it.
Marks `expr` as a JavaScript expression to be emitted **unescaped**. For args whose value must be computed on the client, such as `(raw "evt.target.value")`. Everything else should be plain data and go through the normal escaping — `raw` is an escape hatch, and anything interpolated into it is the caller's responsibility. Never pass user or database content to it.
(write-json x)Serializes x as JSON. Supports what an arg map can hold: nil, booleans,
numbers, strings, keywords, symbols, maps, and sequential collections.
A raw value is emitted verbatim, which is the one case where the caller has
taken responsibility for escaping.
Keywords become strings — a JSON round trip cannot preserve keyword-ness, so
:a arrives back as "a". Handlers receiving enum-like args should compare
against strings, or the adapter should coerce.
Throws on anything else rather than emitting a toString: silently shipping the
print form of an arbitrary object is how defect 2 (parse-long on whatever
arrived) happened in the first place.
Serializes `x` as JSON. Supports what an arg map can hold: nil, booleans, numbers, strings, keywords, symbols, maps, and sequential collections. A `raw` value is emitted verbatim, which is the one case where the caller has taken responsibility for escaping. Keywords become strings — a JSON round trip cannot preserve keyword-ness, so `:a` arrives back as `"a"`. Handlers receiving enum-like args should compare against strings, or the adapter should coerce. Throws on anything else rather than emitting a `toString`: silently shipping the print form of an arbitrary object is how defect 2 (`parse-long` on whatever arrived) happened in the first place.
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 |