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
json1.0.0tinox.core
http_server1.0.4tinox.core
websocket1.0.3tinox.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
    {
        let 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
    {
        let app: ShowcaseApp = this;
        let 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
    {
        let 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
    {
        let widgetJs: String = Fs::readFile("assets/tinox-counter.js");
        let 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

idString
typeString
textString
propsMap<String, String>
childrenList<Component>
visibleBool
@DoNotSerializeonEventfn(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: fn(String) -> Nothing) → Component
fnc textField(value: String, onChange: fn(String) -> Nothing) → Component
fnc checkbox(checked: Bool, onChange: fn(String) -> Nothing) → Component
fnc textArea(value: String, onChange: fn(String) -> Nothing) → Component
fnc numberField(value: Float64, onChange: fn(Float64) -> Nothing) → Component
fnc intField(value: Int64, onChange: fn(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: fn(String) -> Nothing) → Component
fnc link(text: String, path: String, onNavigate: fn(String) -> Nothing) → Component
fnc datePicker(value: String, onChange: fn(String) -> Nothing) → Component
fnc radioGroup(options: List<String>, selected: String, onChange: fn(String) -> Nothing) → Component
fnc fileUpload(uploadUrl: String, showProgress: Bool, onChange: fn(String) -> Nothing) → Component
fnc tabs(labels: List<String>, activeIndex: Int64, content: Component, onTabChange: fn(String) -> Nothing) → Component
fnc dialog(title: String, content: Component, open: Bool, onClose: fn(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: fn(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: fn(String) -> Nothing) → Component
fnc emailField(value: String, onChange: fn(String) -> Nothing) → Component
fnc timePicker(value: String, onChange: fn(String) -> Nothing) → Component
fnc dateTimePicker(value: String, onChange: fn(String) -> Nothing) → Component
fnc slider(value: Float64, min: Float64, max: Float64, onChange: fn(Float64) -> Nothing) → Component
fnc comboBox(options: List<String>, value: String, onChange: fn(String) -> Nothing) → Component
fnc checkboxGroup(options: List<String>, selected: List<String>, onChange: fn(String) -> Nothing) → Component
fnc splitLayout(first: Component, second: Component, initialSplitPercent: Float64) → Component
fnc menuBar(menus: List<String>, onSelect: fn(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: fn(String) -> Nothing, onCancel: fn(String) -> Nothing) → Component
fnc breadcrumb(items: List<String>, paths: List<String>, onNavigate: fn(String) -> Nothing) → Component

class DataGrid<T>

Methods

fnc of(items: List<T>, columns: List<GridColumn<T>>) → Component

class GridColumn<T>

Fields

headerString
accessorfn(T) -> String
componentAccessorfn(T) -> Component
isComponentBool

Methods

fnc of(header: String, accessor: fn(T) -> String) → GridColumn<T>
fnc ofComponent(header: String, componentAccessor: fn(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, fn(String) -> Nothing>
fnc assignIds(node: Component, handlers: Map<String, fn(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, fn(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, fn(String) -> Nothing>, msg: String) → Nothing