domi

package module
v0.0.0-...-e3e3094 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 30, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package domi is a server-rendered framework for building browser applications in Go. An application is a state machine: it implements App, whose View method renders the current state as a Node tree and whose Update method transitions the state in response to events. The framework hosts the application behind an http.Handler, keeps the browser's view in sync with whatever View returns, and dispatches user-generated events back through Update.

The package exposes only the primitives needed to build any node or attribute (Tag, Keyed, Fragment, Text, Name, Group, On). Convenience wrappers for common HTML tags, attributes, and events live in ily.dev/domi/html, ily.dev/domi/attr, and ily.dev/domi/event.

A Node is anything that can appear in the tree: text, an element built via Tag, or a keyed element built via Keyed. Tag and Keyed return curried builders — first attributes, then children. An element with no children is itself a Node, so void elements (e.g. Br, Input) and other childless tags can appear in a parent's child list without a trailing empty children call.

Attribute names beginning with "data-domi-" are reserved for use by this package and its subpackages. Application code and third-party packages should pick data attributes outside that prefix to avoid collisions with framework internals — present or future.

Bundling the Client

The client-side runtime for this module lives in file client.js at the module root. Apps using the default Handler without further customization don't need to access this file directly. The framework includes it in the default document head.

Apps that provide their own document shell can add client.js into their JavaScript bundle alongside their app code. The filesystem path of client.js is:

$(go list -m -f '{{.Dir}}' ily.dev/domi)/client.js

Include this path in your JavaScript bundle, import the module, and call run:

import * as Domi from "path/to/client.js";
Domi.run(); // after document.body is ready

Index

Constants

This section is empty.

Variables

View Source
var (
	// Bypass annotates a link to use the browser's built-in navigation,
	// rather than going through the framework.
	Bypass = Bool("data-domi-bypass")

	// Opaque marks an element as opaque, ignored by the virtual DOM diff.
	// Such an element is inserted,
	// and then never modified until its eventual removal (if any).
	// Any changes to its contents during its existence are ignored.
	// This allows client-side browser code to take ownership of the element
	// without worrying about patches modifying it underfoot.
	//
	// An opaque element must be a keyed child. See [Keyed].
	// Inserting an opaque node anywhere else panics.
	Opaque = Bool("data-domi-opaque")(true)
)

Functions

func Bool

func Bool(name string) func(bool) Attr

Bool returns a builder for a boolean HTML attribute. For standard boolean attributes (disabled, checked, …), true means present (name-only) and false means absent:

Tag("input")(attr.Disabled(true))()   // <input disabled>
Tag("input")(attr.Disabled(false))()  // <input>

For enumerated boolean attributes (contenteditable, draggable, spellcheck, translate), true and false emit the corresponding string value instead:

Tag("div")(attr.ContentEditable(true))()   // <div contenteditable="true">
Tag("div")(attr.ContentEditable(false))()  // <div contenteditable="false">

func Handler

func Handler[Msg any, A App[Msg]](
	f func(context.Context, *url.URL) (A, Cmd[Msg]),
	onURLRequest func(URLRequest) Msg,
	onURLChange func(*url.URL) Msg,
	o ...Option,
) http.Handler

Handler serves an App.

At the start of a session, the returned Handler calls f, providing the initial request URL, to obtain a fresh App instance plus an initial Cmd. The context carries the session ID (see SessionID) and is cancelled when the session ends. This instance is associated with the session, so each browser gets its own independent state.

When the user clicks a link, the framework intercepts the navigation and calls onURLRequest to produce a Msg. The app's Update decides how to handle the request, typically by returning a PushURL or ReplaceURL command.

When the URL changes (from a navigation command or browser back/forward), the framework calls onURLChange to produce a Msg. The app's Update typically translates the URL into a route and updates its state accordingly.

func Keyed

func Keyed(name string) func(...Attr) func(iter.Seq2[string, Node]) Node

Keyed returns a curried builder for an element whose children are paired with stable keys. The framework reconciles updates to keyed children by identity rather than position, so inserting, removing, or reordering items in the middle of a list updates the surviving children in place instead of replacing the entire affected suffix.

The children sequence yields (key, child) pairs in the desired order:

Keyed("ul")(attr.Class("items"))(func(yield func(string, Node) bool) {
    for _, it := range items {
        if !yield(itemKey(it), itemRow(it)) {
            return
        }
    }
})

Each yielded child must be an element; text and Fragment children cannot be keyed, and Keyed panics on a non-element child. Keys must be unique within the sequence and stable across renders for the same logical item.

A child of Keyed can optionally use the Opaque attr. See Opaque for details on its behavior.

func Name

func Name(name string) func(value string) Attr

Name returns a builder for an HTML attribute with the given name. (e.g. class). Call it to obtain an Attr with the given value (e.g. class="foo").

func On

func On(event string) func(msg any) Attr

On returns a builder for an attribute that binds msg to event on the resulting element. When the browser fires the named event, the framework calls Update(msg).

Multiple On(event)(...) attributes on the same element all fire when the event occurs.

If msg has a field tagged domi:"event", the framework unmarshals the event into that field. See ily.dev/domi/event.Event for a convenience type that captures everything the client sends.

If msg cannot be marshaled to JSON, On panics.

func RegisterCombining

func RegisterCombining(name, sep string)

RegisterCombining registers name as a "combining" attribute. When a combining attribute appears more than once in an HTML node, the values are combined, separated by sep, into a single attribute in the rendered output.

RegisterCombining must be called before Handler. This is typically done in an init function in packages that define custom attributes.

The initial set of combining attributes is:

name  sep
class " "
style ";"

func SessionID

func SessionID(ctx context.Context) string

SessionID returns the session ID inside an App or Cmd, otherwise the empty string.

func Tag

func Tag(name string) func(...Attr) Element

Tag returns a curried builder for an HTML element with the given name. Its first call takes attributes, and the second takes children.

Tag("div")(attr.Class("x"))(Text("hi"))

Void elements (and any other "no children" case) can skip the trailing empty children call. Element is itself a Node.

Div()(Text("a"), Br(), Text("b"))

Helpers for common tags can be found in ily.dev/domi/html.

Child nodes must not use the Opaque attr. If a child node is opaque, Tag panics. See Keyed to use opaque nodes.

Types

type App

type App[Msg any] interface {
	// Update is responsible for updating the App state
	// in response to each Msg. It must not produce external
	// side-effects, only update its internal state.
	//
	// The context carries the session ID (see SessionID)
	// as well as values from the HTTP request context, if any.
	// It is cancelled when the session ends.
	//
	// For external side-effects, such as database writes,
	// Update should return a [Cmd].
	Update(context.Context, Msg) Cmd[Msg]

	// View returns the document title and body tree
	// to be displayed in the browser.
	//
	// The context carries the session ID (see SessionID)
	// as well as values from the HTTP request context, if any.
	// It is cancelled when the session ends.
	View(context.Context) (title string, n Node)

	// Subscriptions returns the set of active subscriptions.
	// The framework diffs this set between update cycles.
	// New subscriptions are connected to the App,
	// absent subscriptions are canceled.
	//
	// The context carries the session ID (see SessionID)
	// as well as values from the HTTP request context, if any.
	// It is cancelled when the session ends.
	Subscriptions(context.Context) Sub[Msg]

	// Preview returns the document title and body tree
	// that would be displayed if the browser navigated
	// to the given URL, and whether the navigation is allowed.
	// It must not modify the App's state.
	//
	// The framework calls Preview to pre-render pages
	// the user is likely to visit (e.g. on link hover),
	// so navigation appears instant when the link is clicked.
	// When ok is false the framework discards the title and view
	// and the user's click falls back to normal navigation,
	// giving the app a chance to deny or redirect.
	//
	// The context carries the session ID (see SessionID)
	// as well as values from the HTTP request context, if any.
	// It is cancelled when the session ends.
	Preview(context.Context, *url.URL) (title string, n Node, ok bool)
}

App is the state machine provided by a domi application. One instance holds the state for a single browser sesssion. See Handler for session lifecycle.

type Attr

type Attr interface {
	// contains filtered or unexported methods
}

An Attr is an HTML attribute.

In rendered output, a single attribute name does not appear more than once on any given element:

  1. For each combining attribute, the framework combines the values into a single value. See RegisterCombining for more.
  2. Event handlers are combined internally.
  3. For all other attributes, only the first occurrence appears.

func Group

func Group(a ...Attr) Attr

A Group is a sequence of HTML attributes. It contributes its contents to its parent's child list in order, as if they had been written there directly.

Groups may be nested arbitrarily.

type Cmd

type Cmd[Msg any] struct {
	// contains filtered or unexported fields
}

A Cmd is a deferred side-effect that eventually produces a Msg. The framework runs each Cmd in its own goroutine and passes the resulting Msg back into Update.

func Batch

func Batch[Msg any](c ...Cmd[Msg]) Cmd[Msg]

Batch returns a Cmd that runs each item in c concurrently. The resulting Msg values are dispatched to Update serially.

func Func

func Func[Msg any](f func() Msg) Cmd[Msg]

Func returns a Cmd that runs fn and dispatches its result back into Update.

The app should capture the context from [Update] or the Handler constructor for f to use.

func Load

func Load[Msg any](url string) Cmd[Msg]

Load returns a Cmd that triggers a full-page browser navigation to url, leaving the current session behind. Unlike PushURL and ReplaceURL, which update the history of the running application, Load performs a real navigation (window.location): the browser discards the current document and fetches a fresh one. The url may therefore be absolute and cross-origin.

Load is the escape hatch for links the application does not route itself — logging out, leaving for an external site, or following a same-origin link served outside the domi app. The app returns it from Update in response to a URLRequest it decides not to handle internally. To opt a link out of interception ahead of time, without a server round trip, give the anchor the data-domi-bypass attribute instead.

func PushURL

func PushURL[Msg any](url string) Cmd[Msg]

PushURL returns a Cmd that changes the browser URL and adds an entry to the navigation history. The url must be an origin-relative URL (path, query, fragment) with no scheme or host.

The resulting Cmd dispatches the onURLChange callback registered on Handler so the app can update its route state, and bundles the history.pushState instruction into the same SSE frame as the DOM patches from that update.

func ReplaceURL

func ReplaceURL[Msg any](url string) Cmd[Msg]

ReplaceURL returns a Cmd that changes the browser URL without adding an entry to the navigation history. The url must be an origin-relative URL (path, query, fragment) with no scheme or host.

The resulting Cmd dispatches the onURLChange callback registered on Handler so the app can update its route state, and bundles the history.replaceState instruction into the same SSE frame as the DOM patches from that update.

type Element

type Element func(...Node) Node

Element is the partially-applied builder returned by Tag(name)(attrs). Calling it with children produces a finished element node; an Element with no children is itself a Node, so childless tags can appear in a parent's child list without a trailing empty children call.

Child nodes must not use the Opaque attr. If a child node is opaque, Element panics. See Keyed to use opaque nodes.

type Node

type Node interface {
	// contains filtered or unexported methods
}

A Node is an HTML node, a Text, Raw, Tag, Keyed, or Fragment.

func Fragment

func Fragment(n ...Node) Node

A Fragment is a sequence of HTML nodes. It contributes its contents to its parent's child list in order, as if they had been written there directly.

Fragments may be nested arbitrarily. A Fragment cannot be keyed. Keyed children must each be a single element with an identity.

func Raw

func Raw(s string) Node

Raw returns a node whose content is written verbatim, without HTML escaping. Use Raw for trusted HTML: inline SVG, pre-sanitized markdown output, or fragments from third-party HTML generators. Never pass user-controlled input to Raw without prior sanitization.

The content must produce exactly one DOM node when parsed: either a text string containing no markup, or a single properly nested HTML element with explicit closing tags. Raw panics if the content is empty, contains multiple top-level nodes, or has unclosed tags.

func Safe

func Safe(s string) Node

Safe parses the given HTML string and returns a sanitized Node tree. It uses a hard-coded allowlist of tag names and tag-attribute pairs. Tags not in the allowlist are unwrapped (children preserved). Dangerous tags like script and style are removed entirely, including their children.

func Text

func Text(s string) Node

Text returns a text node. The string is escaped for safe embedding in HTML; use Raw for pre-escaped or trusted content.

func Textf

func Textf(format string, a ...any) Node

Textf returns a text node formatted with fmt.Sprintf.

type Option

type Option interface {
	// contains filtered or unexported methods
}

An Option configures a Handler.

func Document

func Document(f func(title string, body Node) Node) Option

Document supplies a custom builder for the initial HTML shell. The builder is called once per session with the initial document title and the body element. It is responsible for returning a complete html element. The framework always writes the HTML5 doctype declaration before the html element.

domi.Handler(newApp, domi.Document(func(title string, body domi.Node) domi.Node {
    return html.HTML()(
        html.Head()(
            html.Meta(attr.Charset("utf-8")),
            html.Title()(domi.Text(title)),
            html.Script(attr.Type("module"), attr.Src("/bundle.js")),
        ),
        body,
    )
}))

Apps using Document are responsible for loading the Domi client JavaScript. See [Bundling the Client] for details.

func Keepalive

func Keepalive(d time.Duration) Option

Keepalive sets the maximum SSE connection idle time before the server sends an SSE comment line to the client. Keepalives keep proxies from killing an idle connection. The default interval is 25 seconds.

func Logger

func Logger(l *slog.Logger) Option

Logger sets the structured logger used by the framework for internal diagnostics such as malformed client events and handler registry misses. The default logger is slog.Default.

func ReplayWindow

func ReplayWindow(n int) Option

ReplayWindow sets how many recent patch frames a session retains for SSE clients to resume from after a transient disconnect. Clients reconnecting within this window receive only the patches they missed; clients further behind get a full resync of the current view. The default window is 128 frames.

func SessionTimeout

func SessionTimeout(d time.Duration) Option

SessionTimeout sets how long a session can remain idle before the framework releases it. The default timeout is 48 hours.

type Sub

type Sub[Msg any] struct {
	// contains filtered or unexported fields
}

A Sub is a long-lived event source that produces Msg values in response to external stimuli. The zero value of Sub is a valid Sub that emits no messages.

func Subs

func Subs[Msg any](ss ...Sub[Msg]) Sub[Msg]

Subs composes multiple Sub values into one.

func Subscription

func Subscription[Msg any, Key comparable](key Key, f func(context.Context) iter.Seq[Msg]) Sub[Msg]

Subscription creates a Sub that runs f and dispatches each yielded Msg back into Update. The framework uses key to identify this subscription. If a key persists between update cycles, the source stays alive. If it disappears, the source is cancelled.

The Seq returned from f must exit when its context becomes done, in addition to exiting when yield returns false.

type URLRequest

type URLRequest struct {
	URL      *url.URL
	Internal bool
}

A URLRequest represents a user clicking a link in the browser. The framework intercepts the click, prevents the default navigation, and dispatches the request through the onURLRequest callback registered on Handler.

Internal is true when the link target shares the current page's origin (same scheme, host, and port). For internal requests the app typically returns a PushURL command; for external requests it may ignore the event or navigate with a full page load.

Directories

Path Synopsis
Package attr provides prebound constructors for common HTML attributes.
Package attr provides prebound constructors for common HTML attributes.
Package event provides prebound constructors for common DOM event handlers (event.Click, event.Submit, event.Input) and convenience types for the event payload the framework splices into tagged Msg fields.
Package event provides prebound constructors for common DOM event handlers (event.Click, event.Submit, event.Input) and convenience types for the event payload the framework splices into tagged Msg fields.
examples
counter command
nav command
todos command
Package html provides prebound builders for common HTML elements.
Package html provides prebound builders for common HTML elements.
internal
vdom
Package vdom holds the lowered (canonical) form of the domi virtual DOM, plus the renderer and differ that walk it.
Package vdom holds the lowered (canonical) form of the domi virtual DOM, plus the renderer and differ that walk it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL