fields is the contract between a widget’s logic and its displays. The widget declares its data with the field.* constructors; the handle exposes the same keys as typed refs on w.fields; every display prop that shows data binds one of those refs; and every present, push and preview is validated against the schema.
--!strict
type fields_t = {
title: field_ref_t<string>,
playing: field_ref_t<boolean>,
faces: list_ref_t<{ name: field_ref_t<string>, avatar: field_ref_t<string> }>,
chart: record_ref_t<chart_record_t>,
}
local w = widget.new { id = "card", label = "Card", fields = {
title = field.string(""),
playing = field.boolean(false),
faces = field.list { name = field.string(""), avatar = field.image() },
chart = field.record { points = field.numbers(), up = field.boolean(true) },
} }
Export a fields_t type from displays.luau that names one ref per key, exactly as the schema declares them, and type every display function as function(f: fields_t): node_t. A ref that does not exist is then a diagnostic in Studio, and a nil the builder refuses at load.
The constructors
| Constructor | Value pushed | Ref type |
|---|---|---|
field.string(default?) | string | field_ref_t<string> |
field.number(default?) | number | field_ref_t<number> |
field.boolean(default?) | boolean | field_ref_t<boolean> |
field.image() | a URL, a file:// path Perch gave you, a data: URI, or ui.asset("x.png") | field_ref_t<string> |
field.color() | a CSS colour value Lua computed (#hex, hsl(), or the monogram gradient form) | field_ref_t<string> |
field.time_ms() | an epoch in milliseconds, for tickers | field_ref_t<number> |
field.numbers() | an array of numbers: visualizer levels, sparkline points | field_ref_t<{ number }> |
field.list { ... } | an array of records with the element schema given | list_ref_t<E> |
field.record { ... } | a nested record | record_ref_t<R> |
Defaults fill what the first present omits. Records nest to any depth and bind dotted (f.hands.h) or whole (data = f.hands). A list is a top-level field only: field.list inside a record raises field.list() inside a record is not supported; declare the list at the top level.
A provider’s payload is usually wider than your schema. Copy the keys you declared rather than pushing the event table through; an undeclared key raises widget '<id>': unknown field '<key>' (declare it in fields = { … }).
Binding
A data prop takes one of three values:
| Value | Example | Meaning |
|---|---|---|
| a literal | text = "NOW" | fixed |
| a field ref | text = f.title | reads that field of the payload; the node repaints when it changes |
| a function of the data | text = function(d) return d.n .. " left" end | computed in Lua once per push of that widget, then bound like a field |
That is what bind_t<T> means wherever a prop is declared with it. A record binds whole (points = f.chart) or by sub-ref (src = f.solo.avatar); a list binds only to a repeater’s items, whose item builder receives the element’s refs.
Conditions
Every ref builds conditions. when on a node takes a boolean ref or a condition and gates the node’s presence:
ui.icon { glyph = ui.glyph.pause, when = f.paused }
ui.text { text = "Live", when = f.state:eq("live") }
ui.text { text = "Empty", when = f.rows:is_empty() }
ui.box { when = f.count:gt(3):both(f.enabled) }
| Method | On | Holds when |
|---|---|---|
ref:is_true() | any ref | the value is truthy. ui.when(ref) is the same thing |
ref:is_false() | any ref | the value is falsy (false, "", 0) |
ref:eq(v), ref:ne(v) | any ref | the value equals, or differs from, v |
ref:gt(n), ref:lt(n) | number refs | the value is above, or below, n |
ref:is_empty() | string and list refs | the string is "" or the list has no rows |
cond:both(other), cond:either(other), cond:negate_() | conditions | and, or, not |
A gated node starts hidden and shows once its field arrives truthy, so push the gate field along with the rest of the payload.
Style rules
cond:set { ... } turns a condition into a style rule: looks that apply while the condition holds. A node takes up to eight in its style array.
ui.image { src = f.avatar, radius = 999,
style = {
f.speaking:is_true():set { ring = { 2, ui.color.live }, glow = { 12, ui.color.live } },
f.muted:is_true():set { opacity = 0.5 },
} }
The looks a rule may set are color, bg, opacity, weight, ring, glow, gray and z. Never layout.
Types
export type field_ref_t<T> = {
is_true: (self: field_ref_t<T>) -> condition_t,
is_false: (self: field_ref_t<T>) -> condition_t,
eq: (self: field_ref_t<T>, value: T) -> condition_t,
ne: (self: field_ref_t<T>, value: T) -> condition_t,
gt: (self: field_ref_t<T>, value: number) -> condition_t,
lt: (self: field_ref_t<T>, value: number) -> condition_t,
is_empty: (self: field_ref_t<T>) -> condition_t,
}
A typed reference to one declared field. gt and lt are for field_ref_t<number>; is_empty for strings and lists.
export type list_ref_t<E> = { item: E, is_empty: (self: list_ref_t<E>) -> condition_t }
What field.list returns. item holds the element’s refs; ui.list { items = f.faces, key = f.faces.item.name } reads them directly.
export type record_ref_t<R> = R & field_ref_t<any>
What field.record returns: the sub-refs plus a ref to the whole record. Spell a record field as record_ref_t<{ h: field_ref_t<number>, m: field_ref_t<number> }>.
export type clock_record_t = { off: field_ref_t<number>, h24: field_ref_t<boolean> }
export type chart_record_t = { points: field_ref_t<{ number }>, up: field_ref_t<boolean> }
The two records components bind whole: ui.ticker in clock mode takes a clock_record_t, ui.sparkline takes a chart_record_t (up picks stroke over stroke_down).
export type bind_t<T> = T | field_ref_t<T> | ((data: any) -> T)
A bindable value: a literal, a ref, or a derived function.
export type condition_t = {
both: (self: condition_t, other: condition_t) -> condition_t,
either: (self: condition_t, other: condition_t) -> condition_t,
negate_: (self: condition_t) -> condition_t,
set: (self: condition_t, looks: style_looks_t) -> style_rule_t,
}
export type style_rule_t = { __rule: boolean }
export type style_looks_t = { color: color_t?, bg: background_t?, opacity: number?, weight: number?, ring: ring_t?, glow: glow_t?, gray: boolean?, z: number? }
A condition over the widget’s data, the opaque rule set produces, and the looks a rule may flip.
declare field: {
string: (default: string?) -> field_ref_t<string>,
number: (default: number?) -> field_ref_t<number>,
boolean: (default: boolean?) -> field_ref_t<boolean>,
image: () -> field_ref_t<string>,
color: () -> field_ref_t<string>,
time_ms: () -> field_ref_t<number>,
numbers: () -> field_ref_t<{ number }>,
list: <E>(element: E) -> list_ref_t<E>,
record: <R>(shape: R) -> record_ref_t<R>,
}
The constructors, as declared.