ui
Generated by tinox doc
Tinox-UI -- a server-driven web UI framework (Vaadin/Phoenix LiveView-style): write your whole UI as a Tinox component tree running on the server, a thin generic JS client renders it and ships events back over a persistent WebSocket.
| Module | Version | Group |
|---|---|---|
json | 1.1.0 | tinox.core |
http_server | 1.1.0 | tinox.core |
websocket | 1.1.0 | tinox.core |
Annotated counter
import tinox.core.ui
import tinox.core.websocket
import tinox.core.http_server
// Annotation-driven: no HttpServer/WsServer wiring anywhere in the
// program -- @TinoxUIApp generates the HTTP shell/client-JS server and
// the WebSocket accept loop, calling the class's own @View method to
// build/rebuild the component tree (full-resend rendering, automatic
// re-render after every event).
@TinoxUIApp
class CounterApp
{
var clicks: Int64
@View
fn render() -> Component
{
const app: CounterApp = this
return Component::vbox([
Component::label("Clicks: " + this.clicks.toString()),
Component::button("Click me", fn(v: String) { app.clicks = app.clicks + 1 })
])
}
}
Annotated counter entry point
import CounterApp
// Every Tinox program still needs its own class Main as the mandatory
// entry point (since 2026-08-09) -- @TinoxUIApp's generated HTTP/WS
// servers spawn and run alongside it on their own threads, they don't
// replace it. This is the required sibling file for the annotated app
// above.
class Main
{
fnc main() -> Int32
{
return 0
}
}
Widgets and routing
import tinox.core.ui
import tinox.core.websocket
import tinox.core.http_server
// A wider tour of the widget library plus client-side routing:
// Component::link(text, path, onNavigate) reports its target path back
// through the same onEvent/dispatchEvent pipeline every other
// interactive widget uses -- an app switches views by keeping its own
// current-view state (a plain instance field) and branching inside its
// own @View method, no separate routing annotation needed.
@TinoxUIApp
class ShowcaseApp
{
var isAbout: Bool
var quantity: Float64
@View
fn render() -> Component
{
const app: ShowcaseApp = this
const nav: Component = Component::hbox([
Component::link("Home", "/", fn(p: String) { app.isAbout = false }),
Component::link("About", "/about", fn(p: String) { app.isAbout = true })
])
if (this.isAbout)
{
return Component::vbox([nav, Component::heading("About", 1)])
}
return Component::vbox([
nav,
Component::heading("Widgets", 1),
Component::numberField(this.quantity, fn(v: Float64) { app.quantity = v }),
Component::progressBar(this.quantity)
])
}
}
Widgets and routing entry point
import ShowcaseApp
class Main
{
fnc main() -> Int32
{
return 0
}
}
Js component
import tinox.core.ui
import tinox.core.websocket
import tinox.core.http_server
// Component::jsComponent wraps a real browser custom element (a
// Vaadin-@Tag/@JsModule equivalent) instead of dropping to
// Component::html() + a hand-stitched <script> bootstrap whenever the
// v1 widget catalog doesn't cover something (a third-party JS library,
// a stateful client-only widget, ...). `tag` names the custom element;
// `moduleUrl` is dynamic-import()ed once client-side and is expected to
// customElements.define(tag, ...) itself. Every entry in `props`
// becomes a real HTML attribute, so the widget reacts to server-side
// state changes via its own standard observedAttributes/
// attributeChangedCallback -- and since a JsComponent slot's `type`
// never changes across renders, the element is never torn down by an
// unrelated re-render, so its own internal state (an open WebSocket, a
// terminal's scrollback, ...) survives just fine.
@TinoxUIApp
class CounterDemoApp
{
var serverClicks: Int64
var lastReportedFromWidget: String
@View
fn render() -> Component
{
const app: CounterDemoApp = this
var widgetProps: Map<String, String> = Map::new()
widgetProps.insert("label", "server clicks: " + this.serverClicks.toString())
// ":8082/..." -- served by a companion static-asset HttpServer
// (see the entry point below), since @TinoxUIApp's own generated
// HTTP shell (port 8080) can't take extra routes. ui.js expands
// this against the browser's own real hostname at import time --
// see Component::jsComponent's own doc comment, including why the
// companion server must send an Access-Control-Allow-Origin
// header on that response.
return Component::vbox([
Component::label("Server clicks: " + this.serverClicks.toString()),
Component::button("Increment server counter", fn(v: String) { app.serverClicks = app.serverClicks + 1 }),
Component::label("Last value reported by widget: " + this.lastReportedFromWidget),
Component::jsComponent("tinox-counter", ":8082/tinox-counter.js", widgetProps, fn(v: String) { app.lastReportedFromWidget = v })
])
}
}
Js component entry point
import CounterDemoApp
import tinox.core.fs
import tinox.core.http_server
class Main
{
// Serves the widget's own JS module on a companion port, since
// @TinoxUIApp's generated HTTP shell server (port 8080) can't take
// extra custom routes -- see CounterDemoApp's own doc comment.
fnc main() -> Int32
{
const widgetJs: String = Fs::readFile("assets/tinox-counter.js")
const server: HttpServer = HttpServer::new(8082)
server.get("/tinox-counter.js", ctx => {
ctx.response.content(widgetJs, "application/javascript; charset=utf-8")
.header("Access-Control-Allow-Origin", "*")
})
server.listen()
return 0
}
}
class Assets
Methods
fnc shellHtml(wsPort: Int64) → String
fnc shellHtmlTemplate() → String
fnc clientJs() → String
@JsonSerializableclass Component
Fields
id | String | |
type | String | |
text | String | |
props | Map<String, String> | |
children | List<Component> | |
visible | Bool | |
@DoNotSerializeonEvent | fnc(String) -> Nothing |
Methods
fnc inert(value: String) → Nothing
fnc encodePipeList(items: List<String>) → String
fnc none() → Component
fnc label(text: String) → Component
fnc button(text: String, onClick: fnc(String) -> Nothing) → Component
fnc textField(value: String, onChange: fnc(String) -> Nothing) → Component
fnc checkbox(checked: Bool, onChange: fnc(String) -> Nothing) → Component
fnc textArea(value: String, onChange: fnc(String) -> Nothing) → Component
fnc numberField(value: Float64, onChange: fnc(Float64) -> Nothing) → Component
fnc intField(value: Int64, onChange: fnc(Int64) -> Nothing) → Component
fnc heading(text: String, level: Int64) → Component
fnc image(src: String, alt: String) → Component
fnc spacer() → Component
fnc progressBar(value: Float64) → Component
fnc dropdown(options: List<String>, selected: String, onChange: fnc(String) -> Nothing) → Component
fnc link(text: String, path: String, onNavigate: fnc(String) -> Nothing) → Component
fnc datePicker(value: String, onChange: fnc(String) -> Nothing) → Component
fnc radioGroup(options: List<String>, selected: String, onChange: fnc(String) -> Nothing) → Component
fnc fileUpload(uploadUrl: String, showProgress: Bool, onChange: fnc(String) -> Nothing) → Component
fnc tabs(labels: List<String>, activeIndex: Int64, content: Component, onTabChange: fnc(String) -> Nothing) → Component
fnc dialog(title: String, content: Component, open: Bool, onClose: fnc(String) -> Nothing) → Component
fnc notification(message: String, kind: String) → Component
fnc accordionSection(header: String, content: Component, expanded: Bool) → Component
fnc accordion(sections: List<Component>) → Component
fnc gridRow(cells: List<Component>) → Component
fnc gridCell(text: String, isHeader: Bool) → Component
fnc gridCellComponent(content: Component, isHeader: Bool) → Component
fnc vbox(children: List<Component>) → Component
fnc hbox(children: List<Component>) → Component
fnc jsComponent(tag: String, moduleUrl: String, props: Map<String, String>, onEvent: fnc(String) -> Nothing) → Component
fnc html(rawHtml: String) → Component
fn withStyle(style: String) → Component
fn withTooltip(text: String) → Component
fn withDomId(domId: String) → Component
fnc passwordField(value: String, onChange: fnc(String) -> Nothing) → Component
fnc emailField(value: String, onChange: fnc(String) -> Nothing) → Component
fnc timePicker(value: String, onChange: fnc(String) -> Nothing) → Component
fnc dateTimePicker(value: String, onChange: fnc(String) -> Nothing) → Component
fnc slider(value: Float64, min: Float64, max: Float64, onChange: fnc(Float64) -> Nothing) → Component
fnc comboBox(options: List<String>, value: String, onChange: fnc(String) -> Nothing) → Component
fnc checkboxGroup(options: List<String>, selected: List<String>, onChange: fnc(String) -> Nothing) → Component
fnc splitLayout(first: Component, second: Component, initialSplitPercent: Float64) → Component
fnc menuBar(menus: List<String>, onSelect: fnc(String) -> Nothing) → Component
fnc avatar(name: String, imageUrl: String) → Component
fnc initialsOf(name: String) → String
fnc badge(text: String, kind: String) → Component
fnc card(title: String, content: Component) → Component
fnc details(summary: String, content: Component, expanded: Bool) → Component
fnc confirmDialog(title: String, message: String, open: Bool, onConfirm: fnc(String) -> Nothing, onCancel: fnc(String) -> Nothing) → Component
fnc breadcrumb(items: List<String>, paths: List<String>, onNavigate: fnc(String) -> Nothing) → Component
class DataGrid<T>
Methods
fnc of(items: List<T>, columns: List<GridColumn<T>>) → Component
class GridColumn<T>
Fields
header | String | |
accessor | fnc(T) -> String | |
componentAccessor | fnc(T) -> Component | |
isComponent | Bool |
Methods
fnc of(header: String, accessor: fnc(T) -> String) → GridColumn<T>
fnc ofComponent(header: String, componentAccessor: fnc(T) -> Component) → GridColumn<T>
@JsonSerializableclass TinoxUIPatchMessage
Fields
kind | String | |
ops | List<TinoxUIPatchOp> |
@JsonSerializableclass TinoxUIPatchOp
Fields
op | String | |
id | String | |
text | String | |
props | Map<String, String> | |
node | Component | |
parentId | String | |
index | Int64 |
class TinoxUIRuntime
Methods
fnc buildHandlers(root: Component) → Map<String, fnc(String) -> Nothing>
fnc assignIds(node: Component, handlers: Map<String, fnc(String) -> Nothing>, counter: List<Int64>) → Nothing
fnc sendInit(conn: Int64, root: Component) → Nothing
fnc sendUpdate(conn: Int64, root: Component) → Nothing
fnc diff(old: Component, new_: Component, counter: List<Int64>, ops: List<TinoxUIPatchOp>) → Nothing
fnc assignIdsOnly(node: Component, counter: List<Int64>) → Nothing
fnc collectHandlers(node: Component, handlers: Map<String, fnc(String) -> Nothing>) → Nothing
fnc propsEqual(a: Map<String, String>, b: Map<String, String>) → Bool
fnc sendPatch(conn: Int64, ops: List<TinoxUIPatchOp>) → Nothing
fnc dispatchEvent(handlers: Map<String, fnc(String) -> Nothing>, msg: String) → Nothing