dom

package module
v0.13.11 Latest Latest
Warning

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

Go to latest
Published: Sep 8, 2026 License: MIT Imports: 1 Imported by: 0

README

webtyp/dom

Ultra-minimal DOM & reactivity toolkit for Go (TinyGo WASM-optimized).

webtyp/dom provides a type-safe, fine-grained reactive engine over the browser DOM for TinyGo/WASM. State lives in typed Signals; changing a signal patches only the bound DOM node — no Virtual DOM, no manual Update() calls, no re-renders.

Features

  • Fine-Grained Reactivity: SignalString / SignalBool / SignalNodes — O(1) surgical patches that preserve focus and IME composition.
  • Auto-tracking: BindTextFunc / DeriveString discover dependencies automatically — no explicit dep lists.
  • Typed builder: Text, Child, Attr, Class, Set(kv ...fmt.KeyValue) — no Add(...any).
  • Two-method contract: Render() *Element (pure, once per mount) + optional Init(ctx dom.Ctx) (side effects, once ever).
  • Keyed lists & conditional subtrees: BindChildren(SignalNodes) + Show(cond, content).
  • No Virtual DOM: Zero diffing; nodes are never replaced unless structure truly changes.
  • TinyGo Optimized: Zero stdlib; webtyp/fmt for logs; slices over maps; <500KB WASM binaries.
  • Isomorphic: same Render() produces correct SSR HTML on backend and live WASM on frontend.

Installation

go get webtyp.com/dom

Quick Start

import (
    dom "webtyp.com/dom"
    "webtyp.com/fmt"
    "webtyp.com/html"
)

type Counter struct {
    dom.Element
    n     int
    count *dom.SignalString
}

func (c *Counter) Init(ctx dom.Ctx) {
    c.count = dom.NewString("0")
}

func (c *Counter) Render() *dom.Element {
    return html.Div().Child(
        html.Span().BindText(c.count).Class("count"),
        html.Button().Text("Increment").On("click", func(e dom.Event) {
            c.n++
            c.count.Set(fmt.Sprint(c.n))
        }),
    )
}

func main() {
    d := dom.New(...)
    d.Render("app", &Counter{})
}

Component Contract

Method Role Cardinality
Render() *Element Pure: state → structure, no side effects Once per mount
Init(ctx dom.Ctx) Imperative: create signals, load storage, start timers Exactly once (before render)
Mounted() Imperative: DOM operations (measure, focus, scroll) On every insertion

Init and Mounted are optional — only add them when there is work to do.

Reaching Live Elements with Key + Ref

To reach a live DOM node that a component built itself, store the *Element, assign it a .Key(...), and call .Ref() after render:

type RowComp struct {
    dom.Element
    row *dom.Element
}

func (c *RowComp) Render() *dom.Element {
    c.row = html.Span().Key("row").Text("initial")
    return html.Div().Child(c.row)
}

func (c *RowComp) Mounted() {
    if row, ok := c.row.Ref(); ok {
        row.SetText("updated")
    }
}

Ref() answers false until the element has been rendered, so call it from Mounted() or from an event handler — never from Render() itself.

Signals

// String cell — UI text, attr, input state
name := dom.NewString("World")
name.Get()           // "World"
name.Set("Alice")    // notifies all bindings
name.Update(func(v string) string { return v + "!" })

// Bool cell — class/attr toggles, Show conditions
active := dom.NewBool(false)
active.Toggle()

// List of rendered rows — keyed reconcile
rows := dom.NewNodes(elem1, elem2)
rows.Set(newRows)

// Derived (auto-tracking — no deps list)
full := dom.DeriveString(func() string { return first.Get() + " " + last.Get() })

Element Builder

html.Div().
    Class("card").
    Attr("role", "region").
    Text(userInput).                      // Escaped automatically (< → &lt;)
    Raw(dom.Trust("<b>trusted HTML</b>")). // Explicit raw markup (requires dom.Trust)
    Child(
        html.Span().BindText(name),
        html.Input("text").Bind(name),           // two-way
        html.Button().Text("Save").BindAttrBool("disabled", saving),
    )

Builders take no arguments — children go in Child(...) (variadic) and text in .Text(...). The only exceptions are A(href), Input(type), Option(value, text) and SelectedOption(value, text).

Binding methods:

Method DOM target
.BindText(s *SignalString) textContent
.BindAttr(name, s) attribute value
.BindClass(class, on) class toggle
.BindAttrBool(name, on) boolean attribute (disabled, checked…)
.Bind(s) two-way <input>/<textarea>
.BindChildren(s *SignalNodes) keyed child list
.BindTextFunc(fn) computed text (auto-tracking)
.Autofocus() focus on first appearance

Structural:

dom.Show(visible, html.Div().Child(...))  // toggle subtree visibility via display:none
html.Ul().BindChildren(c.rows)                                          // keyed list

Lifecycle

Init (once) → Render → Insert → Wire bindings & events → children Mounted → own Mounted
signal.Set  → patch bound node (O(1))
unmount     → run OnCleanup + unsubscribe signals

Mount Point

Always "app", never "body"Render("body", ...) overwrites innerHTML and destroys the SVG sprite injected by webtyp/sitec.

Dev Mode

dom.SetDevMode(true) // enabled at runtime; default false (production no-op)

When on:

  • Reactive trace: logs signal.Set → patch #node-id
  • BindChildren warns on duplicate/empty keys
  • Nil signal / non-input .Bind / pointer-embedded Element emit warnings instead of panicking

Documentation

License

MIT

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Append

func Append(parentID string, component Component) error

Append injects a component AFTER the last child of the parent element.

func GetDocumentAttr

func GetDocumentAttr(_ string) string

GetDocumentAttr returns an empty string on the backend.

func GetHash

func GetHash() string

GetHash gets the current hash.

func Log

func Log(v ...any)

Log provides logging functionality.

func OnHashChange

func OnHashChange(handler func(hash string))

OnHashChange registers a hash change listener.

func OnScrollCapture

func OnScrollCapture(handler func(scrollTop float64))

OnScrollCapture registra un listener de scroll en FASE DE CAPTURA sobre el documento, de modo que se dispara para CUALQUIER scroller de la página, no solo para la ventana.

Existe porque el evento scroll no burbujea: se dispara únicamente en el elemento que se desplazó. Un shell que quiere reaccionar al scroll de su contenido no puede saber qué descendiente de qué componente es el que realmente desborda, y registrar el listener elemento por elemento lo obligaría a conocer el interior de otros paquetes.

scrollTop es la posición vertical del elemento que disparó el evento. Con varios scrollers en pantalla los valores se intercalan: quien compare posiciones debe tolerarlo con un umbral, no asumir una serie continua.

No hay forma de darlo de baja: es un listener del documento que vive lo que vive la página.

func Render

func Render(parentID string, component Component) error

Render injects a component into a parent element.

func SetDevMode

func SetDevMode(on bool)

SetDevMode enables or disables development mode features.

func SetDocumentAttr

func SetDocumentAttr(_, _ string)

SetDocumentAttr is a no-op on the backend.

func SetHash

func SetHash(hash string)

SetHash sets the current hash.

func SetLog

func SetLog(log func(v ...any))

SetLog sets the logging function.

Types

type Component

type Component interface {
	GetID() string
	SetID(id string)
	String() string
	Children() []Component
}

Component is the minimal interface for components. All components must implement this for both SSR (backend) and WASM (frontend).

NOTE: If your struct embeds Element, embed it as a VALUE, not a pointer:

type MyComponent struct {
  Element       // ✅ Correct — never nil
  // NOT: *Element // ❌ Wrong — nil pointer causes panic in renderToHTML
}

This is because renderToHTML calls GetID() on every Component child before checking ViewRenderer.

type Ctx

type Ctx interface {
	OnCleanup(fn func())
}

Ctx is handed to the Init hook. Register teardown for async resources (timers, websockets).

type DOM

type DOM interface {
	// Render injecta un componente en un elemento padre.
	// 1. Llama a componente.Init(ctx) si existe (una sola vez)
	// 2. Llama a componente.Render() para obtener el árbol de elementos
	// 3. Inyecta el HTML resultante y enlaza bindings y eventos
	Render(parentID string, component Component) error

	// Append injecta un componente DESPUÉS del último hijo del elemento padre.
	// Útil para listas dinámicas.
	Append(parentID string, component Component) error

	// OnHashChange registra un listener para cambios en el hash de la URL.
	OnHashChange(handler func(hash string))

	// OnScrollCapture registra un listener de scroll en fase de captura sobre el
	// documento: se dispara para cualquier scroller de la página. Ver la función
	// de paquete del mismo nombre.
	OnScrollCapture(handler func(scrollTop float64))

	// GetHash devuelve el hash actual de la URL (ej. "#help").
	GetHash() string

	// SetHash actualiza el hash de la URL.
	SetHash(hash string)

	// Get retrieves an element by ID.
	Get(id string) (Reference, bool)

	// Log provides logging functionality using the log function passed to New.
	Log(v ...any)
}

DOM is the main entry point for interacting with the browser. It is designed to be injected into your components.

type Element

type Element struct {
	// contains filtered or unexported fields
}

Element represents a DOM element in the fluent Element API.

func NewElement

func NewElement(tag string) *Element

NewElement creates an Element with the given HTML tag. Used by webtyp/html, webtyp/svg, webtyp/image to build elements.

func Show

func Show(cond *SignalBool, content Component) *Element

Show is implemented for SSR: the child is always serialized; the container carries display:none when cond is false, matching the WASM initial markup.

func (*Element) Attr

func (b *Element) Attr(key, val string) *Element

Attr sets an attribute on the element.

func (*Element) Autofocus

func (b *Element) Autofocus() *Element

Autofocus marks the element to be focused when it first appears.

func (*Element) Bind

func (b *Element) Bind(s *SignalString) *Element

Bind provides two-way binding for <input> and <textarea>.

func (*Element) BindAttr

func (b *Element) BindAttr(name string, s *SignalString) *Element

BindAttr links an attribute to a SignalString.

func (*Element) BindAttrBool

func (b *Element) BindAttrBool(name string, on *SignalBool) *Element

BindAttrBool toggles a boolean attribute (disabled, checked, etc.) based on a SignalBool.

func (*Element) BindAttrBoolFunc

func (b *Element) BindAttrBoolFunc(name string, fn func() bool) *Element

BindAttrBoolFunc toggles a boolean attribute based on a computed boolean.

func (*Element) BindAttrFunc

func (b *Element) BindAttrFunc(name string, fn func() string) *Element

BindAttrFunc links an attribute to a computed string.

func (*Element) BindChildren

func (b *Element) BindChildren(s *SignalNodes) *Element

BindChildren links a container's children to a SignalNodes.

func (*Element) BindClass

func (b *Element) BindClass(class string, on *SignalBool) *Element

BindClass toggles a class based on a SignalBool.

func (*Element) BindClassFunc

func (b *Element) BindClassFunc(class string, fn func() bool) *Element

BindClassFunc toggles a class based on a computed boolean.

func (*Element) BindState

func (b *Element) BindState(s StateAttr, on *SignalBool) *Element

BindState writes the state's attribute while on is true and removes it when false. This is the ONLY supported way to write a widget state: the value the stylesheet selects on comes from the state itself, so markup and CSS cannot disagree.

Not BindAttrBool: that writes the HTML boolean form (`data-x=""`), which no data-state selector matches. That mistake shipped once and was invisible.

func (*Element) BindStateFunc

func (b *Element) BindStateFunc(s StateAttr, fn func() bool) *Element

BindStateFunc is the computed form, for a state derived from more than one signal.

func (*Element) BindText

func (b *Element) BindText(s *SignalString) *Element

BindText links the element's textContent to a SignalString.

func (*Element) BindTextFunc

func (b *Element) BindTextFunc(fn func() string) *Element

BindTextFunc links the element's textContent to a computed string.

func (*Element) Child

func (b *Element) Child(c ...Component) *Element

Child adds one or more elements or components as children.

func (*Element) Children

func (b *Element) Children() []Component

Children returns the component's children (components only).

func (*Element) Class

func (b *Element) Class(class ...string) *Element

Class adds a class to the element.

func (*Element) For

func (b *Element) For(other *Element) *Element

For sets the for= attribute pointing to other's ID, auto-generating other's ID if it has none. Use for label/input pairing and aria-* references.

func (*Element) GetID

func (b *Element) GetID() string

GetID returns the element's ID.

func (*Element) ID

func (b *Element) ID(id string) *Element

ID sets the ID of the element.

func (*Element) Key

func (b *Element) Key(key string) *Element

Key sets a stable identity for keyed reconciliation in BindChildren.

func (*Element) NoCloseTag

func (b *Element) NoCloseTag() *Element

NoCloseTag marks the element as self-closing (no closing tag rendered). Use for void HTML elements: br, hr, img, input, link, meta, etc.

func (*Element) On

func (b *Element) On(t string, h func(Event)) *Element

On adds a generic event handler.

func (*Element) Raw

func (b *Element) Raw(h TrustedHTML) *Element

Raw agrega marcado sin escapar. Exige un TrustedHTML, así que pasar datos de una petición no compila — ver Trust.

func (*Element) Ref added in v0.13.11

func (b *Element) Ref() (Reference, bool)

Ref returns the live DOM node this element was rendered into.

It is the typed alternative to inventing a global id and calling Get on it: the author keeps the *Element they built and asks it for its node, so no name is chosen, and two instances of one component cannot collide.

func (c *Comp) Render() *Element {
	c.row = NewElement("span").Key("row")
	return NewElement("div").Child(c.row)
}
func (c *Comp) onSomething() {
	if row, ok := c.row.Ref(); ok { row.SetText("hi") }
}

ok is false before the element has been rendered, and for an element dom never gave an id — give it a Key to make it addressable. On the backend (SSR) there is no live DOM and Get's stub answer is returned unchanged.

func (*Element) Render

func (b *Element) Render(parentID string) error

Render renders the element to the parent. This is a terminal operation.

func (*Element) Set

func (b *Element) Set(kv ...fmt.KeyValue) *Element

Set applies multiple attributes or classes at once using KeyValue pairs.

func (*Element) SetID

func (b *Element) SetID(id string)

SetID sets the element's ID.

func (*Element) SetState

func (b *Element) SetState(s StateAttr) *Element

SetState writes the state unconditionally, for markup that is born in it.

func (*Element) String

func (b *Element) String() string

String serializes the element tree to its string representation.

func (*Element) Text

func (b *Element) Text(text string) *Element

Text adds a text node child.

type Event

type Event interface {
	// PreventDefault prevents the default action of the event.
	PreventDefault()
	// StopPropagation stops the event from bubbling up the DOM tree.
	StopPropagation()
	// TargetValue returns the value of the event's target element.
	// Useful for input, textarea, and select elements.
	TargetValue() string
	// TargetID returns the ID of the event's target element.
	TargetID() string
	// TargetChecked returns the checked status of the event's target element.
	// Useful for checkbox and radio input elements.
	TargetChecked() bool
}

Event represents a DOM event.

type Reference

type Reference interface {

	// GetAttr retrieves an attribute value.
	GetAttr(key string) string

	// Value returns the current value of an input/textarea/select.
	Value() string

	// SetValue sets element.value (inputs, textarea, select).
	SetValue(value string)

	// SetAttr calls element.setAttribute(key, value).
	// Use empty string for boolean attributes (e.g., SetAttr("disabled", "")).
	SetAttr(key, value string)

	// RemoveAttr calls element.removeAttribute(key).
	RemoveAttr(key string)

	// SetText sets element.textContent.
	// Safe for plain text — does not parse HTML.
	SetText(text string)

	// Checked returns the current checked state of a checkbox or radio button.
	Checked() bool

	// On registers a generic event handler (e.g., "click", "change", "input", "keydown").
	On(eventType string, handler func(event Event))

	// Focus sets focus to the element.
	Focus()

	// ScrollIntoView smooth-scrolls the element into view (e.g. to jump a
	// horizontal scroll-snap container to a different panel programmatically —
	// the browser resolves the final resting position against any
	// scroll-snap-align on this element and its container).
	ScrollIntoView()

	// ScrollIntoViewInstant jumps the element into view with no animation —
	// e.g. a circular scroll-snap strip wrapping from its last panel back to
	// its first, where a smooth scroll would visibly travel across every
	// panel in between in the wrong apparent direction. Every other
	// navigation should keep using ScrollIntoView; reach for this one only
	// at the wrap boundary.
	ScrollIntoViewInstant()

	// ScrollsX reports whether the element can actually scroll along the inline
	// axis — its content is wider than its box.
	//
	// It exists because ScrollIntoView walks EVERY scrollable ancestor, not just
	// the one the caller had in mind. A component that drives a horizontal strip
	// on narrow screens and lays the same panels out side by side on wide ones
	// has to know which it is looking at: on the wide layout the nearest
	// scroller is somebody else's, and scrolling it moves the whole application.
	ScrollsX() bool
}

Reference represents a reference to a DOM node. It provides methods for reading and interaction.

func Get

func Get(id string) (Reference, bool)

Get retrieves an element by ID.

type SignalBool

type SignalBool struct {
	// contains filtered or unexported fields
}

SignalBool — same shape for class/attr toggles and Show conditions.

func DeriveBool

func DeriveBool(compute func() bool) *SignalBool

func NewBool

func NewBool(v bool) *SignalBool

func (*SignalBool) Get

func (s *SignalBool) Get() bool

func (*SignalBool) Set

func (s *SignalBool) Set(v bool)

func (*SignalBool) Toggle

func (s *SignalBool) Toggle()

type SignalNodes

type SignalNodes struct {
	// contains filtered or unexported fields
}

SignalNodes is an observable list of rendered rows. No generics; the component builds the Elements.

func NewNodes

func NewNodes(v ...*Element) *SignalNodes

func (*SignalNodes) Get

func (s *SignalNodes) Get() []*Element

func (*SignalNodes) Set

func (s *SignalNodes) Set(v []*Element)

type SignalString

type SignalString struct {
	// contains filtered or unexported fields
}

SignalString is an observable string cell. UI text/attr/input state lives here. Explicit Get/Set.

func DeriveString

func DeriveString(compute func() string) *SignalString

DeriveString / DeriveBool: read-only computed cells. Re-run automatically when any signal the closure READS changes — no deps argument.

func NewString

func NewString(v string) *SignalString

func (*SignalString) Get

func (s *SignalString) Get() string

func (*SignalString) Set

func (s *SignalString) Set(v string)

func (*SignalString) Update

func (s *SignalString) Update(fn func(string) string)

type StateAttr

type StateAttr interface {
	Key() string
	Value() string
}

StateAttr is anything that names a data-state attribute and the value the stylesheet selects on. widget.State satisfies it; nothing else needs to.

Declared here rather than imported so that dom keeps no dependency on the widget vocabulary — the same seam Class.AsAttr already uses in the other direction.

type TrustedHTML

type TrustedHTML string

TrustedHTML es marcado que el AUTOR del programa garantiza seguro. El tipo existe para que meter datos no confiables en el documento no compile: no hay conversión implícita desde string, y el único constructor obliga a escribir una línea que un grep encuentra.

Regla: sólo literales del propio código, o el resultado de un builder de este ecosistema. NUNCA una cadena que venga de una petición, de una base de datos, de un perfil de OAuth o de otro servicio.

func Trust

func Trust(html string) TrustedHTML

Trust marca html como confiable. Es la ÚNICA forma de producir un TrustedHTML, y su nombre es lo que hace auditable el programa: buscar "dom.Trust(" enumera todos los puntos donde el escapado se saltea a propósito.

Si estás por escribir dom.Trust(algoQueVinoDeAfuera), el defecto está en el diseño del llamador, no acá.

type ViewRenderer

type ViewRenderer interface {
	Render() *Element
}

ViewRenderer returns a Node tree for declarative UI.

Jump to

Keyboard shortcuts

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