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.

ModuleVersionGroup
json (no local docs)1.1.7tinox.core
http_server (no local docs)1.2.8tinox.core
websocket (no local docs)1.1.9tinox.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 fontLinks() → String
fnc shellHtmlTemplate() → String
fnc clientJs() → String

@JsonSerializableclass Component

Fields

idString
typeString
textString
propsMap<String, String>
childrenList<Component>
visibleBool
@DoNotSerializeonEventfnc(String) -> Nothing

Methods

fnc inert(value: String) → Nothing
@Throwsfnc 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
@Throwsfnc 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
@Throwsfnc radioGroup(options: List<String>, selected: String, onChange: fnc(String) -> Nothing) → Component
fnc fileUpload(uploadUrl: String, showProgress: Bool, onChange: fnc(String) -> Nothing) → Component
@Throwsfnc 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
@Throwsfnc comboBox(options: List<String>, value: String, onChange: fnc(String) -> Nothing) → Component
@Throwsfnc checkboxGroup(options: List<String>, selected: List<String>, onChange: fnc(String) -> Nothing) → Component
fnc splitLayout(first: Component, second: Component, initialSplitPercent: Float64) → Component
@Throwsfnc 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

headerString
accessorfnc(T) -> String
componentAccessorfnc(T) -> Component
isComponentBool

Methods

fnc of(header: String, accessor: fnc(T) -> String) → GridColumn<T>
fnc ofComponent(header: String, componentAccessor: fnc(T) -> Component) → GridColumn<T>

@JsonSerializableclass TinoxUIPatchMessage

Fields

kindString
opsList<TinoxUIPatchOp>

@JsonSerializableclass TinoxUIPatchOp

Fields

opString
idString
textString
propsMap<String, String>
nodeComponent
parentIdString
indexInt64

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

@Errorclass UiError

* Invalid UI component content.

Fields

messageString