Statements
Studio writes this format for you. The statement editor ( Statements ) builds these bodies as visual step cards, and this page documents the JSON those cards write to disk.
A
Function entry
's
body
may be a
JSON array of statements
instead of a JavaScript source string. Each statement is one step; steps run top to bottom, exactly like the statement list inside a JavaScript function, but as structure, so tooling can validate, inspect, and edit every step.
The smallest complete structured body mutates, then notifies:
{
"addItem": {
"$prototype": "Function",
"body": [
{ "operator": "push", "target": { "$ref": "#/state/items" }, "value": "New item" },
{ "dispatchEvent": "items-changed" }
]
}
}
There are six statement kinds, each reusing a web-platform name:
| Kind | Shape | Source of the name |
|---|---|---|
| Expression | a bare expression node (mutation or
call
) |
ECMAScript ExpressionStatement |
| Branch | { "if", "then", "else"? } |
JSON Schema conditional keywords |
| Multiway | { "$switch", "cases", "default"? } |
Element-level
$switch
, ECMA switch |
| Dispatch | { "dispatchEvent", "detail"?, "bubbles"?, "composed"? } |
WHATWG DOM
dispatchEvent |
| Stop | { "stopPropagation": true } |
WHATWG DOM
stopPropagation() |
| Cancel | { "preventDefault": true } |
WHATWG DOM
preventDefault() |
Expression statements
Any mutating
expression node
, or a
call
, stands alone as a step. No wrapper key; the node appears bare in the array:
{ "operator": "+=", "target": { "$ref": "#/state/count" }, "value": 1 }
To capture a result, make the step an assignment whose
value
is a
call
node. There is no dedicated capture field:
{
"operator": "=",
"target": { "$ref": "#/state/total" },
"value": { "operator": "call", "target": { "$ref": "#/state/computeTotal" } }
}
Branches
if
holds a
pure
expression operand;
then
and
else
hold statement lists.
else
is optional:
{
"if": { "operator": ">", "target": { "$ref": "#/state/cart/length" }, "value": 10 },
"then": [{ "dispatchEvent": "cart-full" }],
"else": [{ "operator": "call", "target": { "$ref": "#/state/refreshTotals" } }]
}
Multiway branches
$switch
mirrors the element-level
$switch
in statement position: the discriminant is a pure operand,
cases
maps its
string form
to statement lists, and the optional
default
runs when no key matches:
{
"$switch": { "$ref": "#/state/status" },
"cases": {
"error": [{ "dispatchEvent": "load-failed" }],
"ready": [{ "operator": "=", "target": { "$ref": "#/state/visible" }, "value": true }]
},
"default": []
}
Dispatching events
dispatchEvent
fires a DOM
CustomEvent
, and the statement's other keys mirror
CustomEventInit
.
detail
may be a literal, a
$ref
, or an expression node:
{
"dispatchEvent": "cart-changed",
"detail": { "$ref": "#/state/cart" },
"bubbles": true,
"composed": true
}
The event dispatches from the handler's
event.currentTarget
(or the component instance in compiled custom elements). Declare the events a function fires in its
emits
array so Studio can autocomplete them.
Stopping and cancelling events
stopPropagation
and
preventDefault
act on the event the handler is running for. Each is written as the member set to
true
, so the step reads as the call it becomes:
{
"onclick": {
"$prototype": "Function",
"body": [
{ "stopPropagation": true },
{ "preventDefault": true },
{ "dispatchEvent": "select", "detail": { "$ref": "#/state/value" }, "bubbles": true }
]
}
}
Stop the event when an ancestor handles the same one and must not see this occurrence: a row nested inside another row, a button inside a clickable card. Cancel the default when the element's built-in behaviour is not wanted: a link that opens in place, a form the body submits itself, an arrow key that would otherwise scroll. Outside a handler, where there is no event, both steps do nothing.
Parameters
A structured body follows the named-formula pattern. Without
parameters
, the entry is an event handler, and statements see
state
and the
event#/
scheme. With
parameters
, it is a callable invoked positionally, its arguments bound to
$args/
names:
{
"addToCart": {
"$prototype": "Function",
"parameters": ["item"],
"body": [
{
"operator": "push",
"target": { "$ref": "#/state/cart" },
"value": { "$ref": "$args/item" }
}
]
}
}
Statements, expressions, or a body string?
The escalation ladder continues inside Shape 4. Prefer the least powerful form:
| Form | Use when |
|---|---|
A single
$expression |
One operation: toggle, increment, push, a computed value |
body
as a statement array |
Multiple steps, branching, or dispatching events, still with no code |
body
as a JavaScript string |
Loops,
await
chains, try/catch, browser APIs: anything statements can't express |
A statement body stays fully analyzable and visually editable; a source string is opaque to tooling. Reach for the string only when structure genuinely runs out.
How it works
One engine serves both halves: an interpreter (
runStatements
) executes statement arrays directly against the reactive
state
proxy, and a compiler (
compileStatements
) emits the genuine ECMAScript forms:
if
/
else
statements, a
switch
over the discriminant's string form,
dispatchEvent(new CustomEvent(type, init))
, and
event.stopPropagation()
/
event.preventDefault()
. The emitted function is identical in shape to a hand-written handler, so
reactivity
tracking works unchanged.
Statements execute sequentially. A step whose value is a promise is awaited before the next step runs, giving async/await semantics without writing
await
.
Rules
A
bodyis either a statement array or a source string, one representation per entry.Branch tests (
if) and$switchdiscriminants are pure operands: no mutations, nocallto a mutating entry.$switchmatchesString(discriminant)againstcaseskeys, because JSON keys are strings.Statements run in order; thenable results are awaited before the next step.
There is no capture field; assign a
call's result with an=statement.dispatchEventneeds a dispatch target; outside a handler (noevent), a compiled custom element dispatches from the component instance.Declare dispatched events in
emits, which is the declaration tooling reads.stopPropagationandpreventDefaultneed the handler's event; outside a handler they do nothing.
Related
Expressions : the nodes statements are built from
Functions and sidecars : the entries that own a
bodySwitching :
$switchat the element levelStatements in Studio : the visual step editor
Code editing in Studio : the JavaScript escape hatch