Liking cljdoc? Tell your friends :D

darkstar.action

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.

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.
raw docstring

actionclj

(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.
sourceraw docstring

deleteclj

(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`.
sourceraw docstring

getclj

(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`.
sourceraw docstring

patchclj

(patch path event)
(patch path event args)
(patch path event args opts)

A @patch action expression. See action.

A `@patch` action expression. See `action`.
sourceraw docstring

postclj

(post path event)
(post path event args)
(post path event args opts)

A @post action expression. See action.

A `@post` action expression. See `action`.
sourceraw docstring

putclj

(put path event)
(put path event args)
(put path event args opts)

A @put action expression. See action.

A `@put` action expression. See `action`.
sourceraw docstring

rawclj

(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.
sourceraw docstring

write-jsonclj

(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.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close