Ultra-minimal DOM & event toolkit for Go (TinyGo WASM-optimized).
tinywasm/dom provides a minimalist, WASM-optimized way to interact with the browser DOM in Go, avoiding the overhead of the standard library and syscall/js exposure. It is designed specifically for TinyGo applications where binary size and performance are critical.
- JSX-like Declarative View: Concise nesting with
Div(H1("Title"), P("...")) - Typed Form Elements: Semantic API for forms with
Text("email").Required() - Void Element Fix: Correctly renders
<br>,<img>,<input>without closing tags - TinyGo Optimized: Avoids heavy standard library packages to keep WASM binaries <500KB
- Direct DOM Manipulation: No Virtual DOM overhead. You control the updates.
- ID-Based Caching: Efficient element lookup and caching strategy
- Lifecycle Hooks:
OnMount,OnUpdate,OnUnmountfor fine-grained control
go get github.com/tinywasm/domFor a complete example including Elm architecture (Dynamic Components) and Static Components, check the following file:
π web/client.go
This file contains the reference implementation used for testing and demonstrations.
The API allows concise nesting and typed chaining:
import . "github.com/tinywasm/dom"
Div(
H1("Welcome"),
P("Enter your credentials:"),
Form(
Email("user_email", "Email address").Required(false),
Password("pwd").Placeholder("Secret password"),
Button("Login").Attr("type", "submit"),
).Action("/login"),
).Class("container")Available builders:
- Containers:
Div,Span,P,H1-H6,Ul,Ol,Li,Section,Main,Article,Header,Footer,Nav,Aside,Table,Thead,Tbody,Tr,Td, etc. - Typed Inputs:
Text,Email,Password,Number,Checkbox,Radio,File,Date,Hidden,Search,Tel,Url,Range,Color. - Specialized:
Form,Select,Textarea,Button,A. - Void Elements:
Img,Br,Hr.
Components can implement optional lifecycle interfaces:
type MyComponent struct {
*dom.Element
data []string
}
// Called after component is mounted to DOM
func (c *MyComponent) OnMount() {
c.data = fetchData()
c.Update()
}
// Called after re-render (dom.Update)
func (c *MyComponent) OnUpdate() {
fmt.Println("Component updated")
}
// Called before component is removed
func (c *MyComponent) OnUnmount() {
// Cleanup resources
}All components must implement:
type Component interface {
GetID() string
SetID(string)
RenderHTML() string // OR Render() *Element
Children() []Component
}Two rendering options:
RenderHTML() string- For static components (smaller binary)Render() *dom.Element- For dynamic components (type-safe, composable)
Components can implement either or both. DOM checks Render() first, falls back to RenderHTML().
Choose the right rendering method for each component:
| Component Type | Method | Benefit |
|---|---|---|
| Static (no interactivity) | RenderHTML() string |
Smaller binary, less overhead |
| Dynamic (interactive, state) | Render() *dom.Element |
Type-safe, composable, fluent API |
See the implementation examples in web/client.go to see both approaches in action.
Components can contain child components:
type MyList struct {
*dom.Element
items []dom.Component
}
func (c *MyList) Children() []dom.Component {
return c.items
}
func (c *MyList) Render() *dom.Element {
list := dom.Div()
for _, item := range c.items {
list.Add(item) // Components can be children
}
return list
}When you call dom.Render("app", myList), the library will:
- Render the HTML
- Call
OnMount()forMyList - Recursively call
OnMount()for allitems
The same recursion applies to cleanup, ensuring all event listeners are cleaned up when a parent is replaced.
Event handling is integrated directly into the Builder API via On(eventType, handler).
// Rendering
dom.Render(parentID, component) // Replace parent's content
dom.Append(parentID, component) // Append after last child
dom.Update(component) // Re-render in place
// Routing (hash-based)
dom.OnHashChange(handler) // Listen to hash changes
dom.GetHash() // Get current hash
dom.SetHash(hash) // Set hashEmbedding *dom.Element provides these methods automatically:
type Counter struct {
*dom.Element
count int
}
// Chainable helpers
counter.Update() // Trigger re-render
counter.GetID() // Get unique ID
counter.SetID("my-id") // Set custom IDFor more detailed information, please refer to the documentation in the docs/ directory:
- Specification & Philosophy: Design goals, architecture, and key decisions.
- API Reference: Detailed definition of
DOM,Element, andComponentinterfaces. - Creating Components: Guide to building basic and nested components.
- Event Handling: Using the
Eventinterface for clicks, inputs, and forms. - Advanced Patterns: Dynamic lists, decoupling, and performance tips.
- Comparison: TinyDOM vs. syscall/js, VDOM, and JS frameworks.
-
β Major API Redesign - JSX-like factories (
Div(H1("Title"))) -
β Typed Form Elements - Semantic chaining (
Email("u").Required()) -
β Internal Privatization - Cleaned up public API (privatized
EventHandler, etc.) -
β Void Element Rendering - Correct HTML for
<br>,<img>,<input> -
β Auto-ID Generation - Simplified IDs without
auto-prefix -
β JSX-like factories - Concise nesting (
Div(H1("Title"), P("..."))) -
β Typed Form Elements - Semantic chaining (
Email("u").Required()) -
β Void Element Rendering - Correct HTML for
<br>,<img>,<input> -
β Fluent Builder API - Chainable methods (
dom.Div().ID("x").Class("y")) -
β Hybrid rendering - Choose DSL or string HTML per component
-
β Lifecycle hooks -
OnMount,OnUpdate,OnUnmount -
β Auto-ID generation - All components get unique IDs automatically
Binary Size (TinyGo WASM):
- Simple counter app: ~35KB (compressed)
- Todo list with 10 components: ~120KB (compressed)
- Full application: <500KB (compressed)
Compared to standard library approach: 60-80% smaller binaries.
MIT