Skip to content
Streamlord

The $ trap

Kotlin's dollar and Datastar's dollar are the same character and not the same thing.

Datastar reads $count as a signal. Kotlin reads $count as a string template. In a Kotlin string, Kotlin wins:

dataOnClick("$count++")     // sends "++" to the browser
dataText("$user.name")      // sends ".name"

If count is not in scope, that line does not compile and you find out immediately. If a count is in scope (and in a handler that has just read signals, one often is), it compiles, ships, and Datastar ignores the malformed expression without a word. The page simply does nothing.

What Streamlord does about it

Every expression helper runs the text through ExpressionGuard, which throws InterpolatedExpressionException at render time when the text has the shape only an eaten signal leaves behind: empty, only operators, or an operator with a missing side.

A stack trace is not a fix, but it is an enormous improvement on a page that quietly does nothing.

Three ways to write it right

In order of preference:

button { dataOnClick(increment("count")) }

The helpers (signal, set, toggle, not, increment, decrement, statements) build the expression for you, and there is no dollar in your source at all. This is the one to reach for.

button { dataOnClick($$"$count++") }

Kotlin 2.2 and newer. In a $$"..." literal, a single $ is just a dollar and $$ opens a template. Good when the expression is genuinely JavaScript that no helper covers.

button { dataOnClick("${'$'}count++") }

Works on any Kotlin version, and reads as badly as it looks. Use it when you are stuck on an older compiler.

For strings and templates

The DSL guards its own expressions because it builds them. HTML that arrives as a string has to be walked instead, and that is guardElements, which is opt-in, because walking every patch is a choice you should make rather than inherit.

Templates have no trap at all: the template engine owns the file, and Kotlin never sees the dollar.

In the editors

Both the VS Code extension and the IntelliJ plugin flag the trap while you type, with a quick fix that rewrites the line into one of the three forms above. That is the cheapest place to catch it: before the code exists, rather than in a stack trace or, worse, in a page that does nothing.

What went over the wire

The frames your last search produced, encoded by the same SseEncoder the golden-file tests check. Not a description of them. The frames.

Nothing yet. Search from the top of the page, and what the server sends will appear here.