A program can be more than a static layout. It can hold state, compute values and react to input, all in the browser, with no round trip to the model.
State
$name = value declares a state variable. bind:$name ties a control to it, both ways.
Loading preview…
$plan = "Team"
$seats = 12
root = Card(RadioGroup("plan", "Plan", ["Starter", "Team", "Enterprise"], v:segmented, bind:$plan), Slider("seats", "Seats", min:1, max:100, bind:$seats, unit:" seats"), Stat("Monthly cost", @fmt($seats * ($plan == "Starter" ? 0 : $plan == "Team" ? 29 : 79), "$"), note:$plan + " plan"))bind: works on Input, TextArea, Select, Combobox, RadioGroup, CheckboxGroup, TagInput, Checkbox, Switch, Slider, DatePicker, TimePicker, Tabs (the selected label) and Chart (the selected x).
A host can set starting values with initialState, and listen with onStateChange.
Operators
| Kind | Operators |
|---|---|
| Arithmetic | + - * / % |
| Comparison | == != < > <= >= |
| Logic | && ` |
| Conditional | a ? b : c |
| Access | a.b, a[0] |
+ joins text when either side is text. A conditional can choose between components:
body = $tab == "done" ? doneView : formViewLists and @each
A pipe table, or a query result, is a list of rows. @each makes one component per row.
Loading preview…
root = Stack(Header("Large orders", "Over $1,000", size:sm), Grid(big, cols:3))
big = @each(@filter(orders, o => o.Total > 1000), o => Tile(o.Customer, o.Status, value:@fmt(o.Total, "$"), tone:o.Status == "Refunded" ? "danger" : "success"))
orders = |Customer|Total:n|Status
|Lumen Labs|1200|Paid
|Kite & Co|240|Paid
|Brightwave|9800|Paid
|Orbital|4300|Refundedrows.total plucks one field from every row, so @sum(orders.Total) adds a column.
Built-in functions
| Function | Returns |
|---|---|
@count(list) |
number of items |
@first(list), @last(list) |
one item |
@sum(list), @avg(list), @min(list), @max(list) |
a number |
@sort(list, key) |
a sorted list |
@filter(list, x => test) |
the items that pass |
@each(list, x => Component) |
one component per item |
@join(list, sep) |
text |
@round(n, digits), @abs(n), @floor(n), @ceil(n) |
a number |
@fmt(value, kind) |
formatted text; kind is "$", "%", "compact" or "date" |
There is nothing else. A program cannot define functions, loop without bound or call into the page.
Queries and mutations
sales = @query("get_sales", {range:$range}, default:[], every:60)
save = @mutation("update_goal", {goal:$goal})A query reads through your tools and re-runs when its arguments change. A mutation runs only from a button. Both are covered in Tools and live data.
Action lists
do:[…] on a Button, Tile, Media, Hero or Pricing runs steps in order and stops at the first failure.
btn = Button("Save", do:[@run(save), @set($tab, "done"), @send("Goal saved")])| Step | Effect |
|---|---|
@set($x, value) |
set state |
@reset($x, …) |
back to the declared defaults |
@run(name) |
run a mutation, or re-fetch a query |
@send("text") |
a send action to your app |
@open(url) |
open a link |
@emit("event", payload) |
a custom event to your app |
What reaches your onAction is described in Actions and forms.
How it is evaluated
- Expressions are interpreted, never compiled: no
eval, nonew Function. - Each expression tracks what it read. A state change re-evaluates only the props that depend on it.
- Evaluation is bounded: 500,000 steps per pass, 5,000 items per
@each. See limits.