node

package
v0.3.2 Latest Latest
Warning

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

Go to latest
Published: May 5, 2026 License: MIT Imports: 4 Imported by: 6

Documentation

Overview

Package node defines the interfaces and helpers that compose a fluent render tree. Every renderable item satisfies Node: HTML elements, text, conditionals, function wrappers, and memoised nodes.

Tree-level helpers create nodes without importing an HTML element package: Func, Funcs, Condition, When, Unless, and Memoise.

All node types that produce dynamic content support reactive tracking via the Dynamic interface. Call .Dynamic(key) on elements, function components, and conditionals to assign a tracking key. The diff engine in fluent-jit (and Tether's reactive UI) uses these keys to produce targeted patches when content changes between renders.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Attribute

type Attribute struct {
	// Key is the attribute name as it appears in the rendered HTML
	// (for example "class", "data-id", or "hx-get"). It is written
	// verbatim - callers are expected to pass a valid attribute name.
	Key string

	// Value is the attribute value as it appears in the rendered HTML.
	// It is written verbatim and is not HTML-escaped, so callers that
	// accept untrusted input must escape before storing.
	Value string
}

Attribute is a single HTML attribute pair held in an element's generic attribute slice. Typed attributes live in dedicated struct fields; this type backs the catch-all storage that [Element.SetAttribute], SetAria, and SetData write into.

type ConditionalBuilder

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

ConditionalBuilder provides a fluent API for conditional rendering. It allows you to specify different content based on a boolean condition.

Nil nodes are safely ignored - if a nil node is provided to True() or False(), it will not be stored and nothing will be rendered for that path.

Chain .Dynamic("key") to enable diff engine tracking. Without a key, the differ cannot produce targeted patches for conditional content.

Usage:

node.Condition(user.IsLoggedIn).
    True(p.Text("Welcome back!")).
    False(p.Text("Please log in")).
    Dynamic("auth-message")

func Condition

func Condition(condition bool) *ConditionalBuilder

Condition creates a new conditional builder with the given boolean condition.

func Unless

func Unless(condition bool, node Node) *ConditionalBuilder

Unless renders the node only when the condition is false. This is a shorthand for Condition(cond).False(node).

Usage:

Unless(user.IsLoggedIn, a.New().Href("/login").Text("Sign in"))

func When

func When(condition bool, node Node) *ConditionalBuilder

When renders the node only when the condition is true. This is a shorthand for Condition(cond).True(node).

Usage:

When(user.IsAdmin, span.Static("Admin"))

func (*ConditionalBuilder) Dynamic added in v0.3.0

func (c *ConditionalBuilder) Dynamic(key string) *ConditionalBuilder

Dynamic marks this conditional for reactive tracking by the diff engine. The key identifies this node across renders so the diff engine can detect changes and send targeted patches.

func (*ConditionalBuilder) DynamicKey added in v0.2.0

func (c *ConditionalBuilder) DynamicKey() string

DynamicKey returns the developer-assigned key for diff engine tracking. Returns an empty string if the conditional has not been marked as dynamic.

func (*ConditionalBuilder) False

func (c *ConditionalBuilder) False(node Node) *ConditionalBuilder

False sets the node to render when the condition is false. If node is nil (explicit or typed nil pointer), it is not stored.

func (*ConditionalBuilder) IsDynamic added in v0.2.0

func (c *ConditionalBuilder) IsDynamic() bool

IsDynamic returns true - conditionals always contain dynamic content because their output depends on a runtime condition.

func (*ConditionalBuilder) Nodes

func (c *ConditionalBuilder) Nodes() []Node

Nodes returns only the active branch - the one that Render will actually produce output for. Returning both branches would mislead tree walkers into believing that inactive content exists in the rendered tree, which breaks the Differ's structural change detection for keyed elements.

func (*ConditionalBuilder) Render

func (c *ConditionalBuilder) Render(w ...io.Writer) []byte

Render generates the HTML representation based on the condition. If a writer is provided, the output is written to it and nil is returned. If no writer is provided, the output is returned as a byte slice.

func (*ConditionalBuilder) RenderBuilder

func (c *ConditionalBuilder) RenderBuilder(buf *bytes.Buffer)

RenderBuilder writes the HTML representation directly to a buffer. Renders the appropriate node based on the condition.

func (*ConditionalBuilder) True

True sets the node to render when the condition is true. If node is nil (explicit or typed nil pointer), it is not stored.

type Dynamic

type Dynamic interface {
	IsDynamic() bool
	DynamicKey() string
}

Dynamic represents nodes that contain dynamic content requiring re-evaluation on each render. The fluent-jit diff engine and Tether's reactive UI use this interface to identify trackable nodes in the render tree.

IsDynamic reports whether the node produces different output across renders. DynamicKey returns the developer-assigned key used by the diff engine to track changes. Nodes without a key return an empty string and are not individually tracked.

HTML elements, FunctionComponent, FuncsComponent, and ConditionalBuilder all satisfy this interface. Call .Dynamic(key) on any of them to assign a tracking key.

type Element

type Element interface {
	Node

	// SetAttribute is the escape hatch for attributes that Fluent's typed
	// API does not cover - framework directives like Alpine.js (x-on:click)
	// or HTMX (hx-get), and any other custom or non-standard attribute.
	//
	// For everything else, prefer the typed methods on each element
	// (.Class, .Href, .Checked, ...). They produce the right escaping,
	// validate enumerated values at compile time, and chain. SetAttribute
	// returns nothing on purpose - it cannot be chained, which keeps the
	// Fluent API the obvious default.
	//
	// For ARIA attributes use SetAria(name, value); for data-* attributes
	// use SetData(name, value). Both return the element for chaining.
	SetAttribute(key string, value string)

	// RenderOpen writes the opening tag, including the element name and
	// every attribute, into buf. JIT compilation caches this output
	// separately from the children so static wrappers can be pre-rendered
	// while dynamic content re-evaluates each render. For <div class="x">
	// it writes the entire opening fragment.
	RenderOpen(buf *bytes.Buffer)

	// RenderClose writes the closing tag (or the self-closing terminator
	// for void elements) into buf. Paired with RenderOpen so the JIT can
	// cache the open and close fragments independently of the children
	// rendered between them.
	RenderClose(buf *bytes.Buffer)
}

Element extends Node for HTML elements that have attributes and an open/close tag structure. Not all nodes are elements - text nodes, function components, and conditionals are not. This separation allows extensions (htmx, turbo, shoelace) to accept only types that genuinely support attributes, and allows JIT compilation to pre-render static wrapper tags independently of dynamic content.

type FuncsComponent added in v0.2.0

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

FuncsComponent enables dynamic content generation of multiple nodes. The function is called during rendering to generate the actual node content. This is useful for generating lists of items, e.g. from a loop.

Nil nodes are safely ignored - any nil nodes in the returned slice will be skipped.

Chain .Dynamic("key") to enable diff engine tracking. Without a key, the differ cannot produce targeted patches for content generated by the function.

Usage:

node.Funcs(func() []Node {
    nodes := []Node{}
    for _, item := range items {
        nodes = append(nodes, li.Text(item.Name))
    }
    return nodes
}).Dynamic("item-list")

func Funcs added in v0.2.0

func Funcs(fn func() []Node) *FuncsComponent

Funcs creates a new function component that will call the provided function during rendering to generate a slice of nodes. It is the plural form of Func - use Func when the function returns a single Node, and Funcs when it returns a slice.

func Map added in v0.3.2

func Map[T any](items []T, fn func(T) Node) *FuncsComponent

Map is a convenience over Funcs for the common case of producing one node per element of a slice. It removes the boilerplate of allocating the result slice and looping, so templates can inline the transformation.

Usage:

node.Map(categories, func(c domain.Category) node.Node {
    return label.New(
        input.New().Type(inputtype.Checkbox).Value(strconv.Itoa(c.ID)).
            Checked(slices.Contains(selected, c.ID)),
        text.Text(c.Name),
    )
})

Chain .Dynamic("key") on the returned component to enable diff engine tracking.

func (*FuncsComponent) Dynamic added in v0.3.0

func (f *FuncsComponent) Dynamic(key string) *FuncsComponent

Dynamic marks this function component for reactive tracking by the diff engine. The key identifies this node across renders so the diff engine can detect changes and send targeted patches.

func (*FuncsComponent) DynamicKey added in v0.2.0

func (f *FuncsComponent) DynamicKey() string

DynamicKey returns the developer-assigned key for diff engine tracking. Returns an empty string if the component has not been marked as dynamic.

func (*FuncsComponent) IsDynamic added in v0.2.0

func (f *FuncsComponent) IsDynamic() bool

IsDynamic returns true - function components always contain dynamic content because their output depends on a function call at render time.

func (*FuncsComponent) Nodes added in v0.2.0

func (f *FuncsComponent) Nodes() []Node

Nodes evaluates the function and returns its output. Returning an empty slice would hide any keyed children from tree walkers, which breaks the Differ's ability to detect patches and structural changes for elements generated by function components.

func (*FuncsComponent) Render added in v0.2.0

func (f *FuncsComponent) Render(w ...io.Writer) []byte

Render generates the HTML representation by calling the function. If a writer is provided, the output is written to it and nil is returned. If no writer is provided, the output is returned as a byte slice.

func (*FuncsComponent) RenderBuilder added in v0.2.0

func (f *FuncsComponent) RenderBuilder(buf *bytes.Buffer)

RenderBuilder writes the HTML representation directly to a buffer. Calls the function to get the actual nodes and renders them. Nil nodes are safely ignored.

type FunctionComponent

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

FunctionComponent enables dynamic content generation through function calls. The function is called during rendering to generate the actual node content. This is useful for complex conditional logic, loops, and data transformations.

Nil nodes are safely ignored - if the function returns nil, nothing will be rendered.

Chain .Dynamic("key") to enable diff engine tracking. Without a key, the differ cannot produce targeted patches for content generated by the function.

Usage:

node.Func(func() Node {
    if user.IsLoggedIn {
        return div.Text("Welcome back!")
    }
    return div.Text("Please log in")
}).Dynamic("greeting")

func Func

func Func(fn func() Node) *FunctionComponent

Func creates a new function component that will call the provided function during rendering to generate the actual node content.

func (*FunctionComponent) Dynamic added in v0.3.0

func (f *FunctionComponent) Dynamic(key string) *FunctionComponent

Dynamic marks this function component for reactive tracking by the diff engine. The key identifies this node across renders so the diff engine can detect changes and send targeted patches.

func (*FunctionComponent) DynamicKey added in v0.2.0

func (f *FunctionComponent) DynamicKey() string

DynamicKey returns the developer-assigned key for diff engine tracking. Returns an empty string if the component has not been marked as dynamic.

func (*FunctionComponent) IsDynamic added in v0.2.0

func (f *FunctionComponent) IsDynamic() bool

IsDynamic returns true - function components always contain dynamic content because their output depends on a function call at render time.

func (*FunctionComponent) Nodes

func (f *FunctionComponent) Nodes() []Node

Nodes evaluates the function and returns its output. Returning an empty slice would hide any keyed children from tree walkers, which breaks the Differ's ability to detect patches and structural changes for elements generated by function components.

func (*FunctionComponent) Render

func (f *FunctionComponent) Render(w ...io.Writer) []byte

Render generates the HTML representation by calling the function. If a writer is provided, the output is written to it and nil is returned. If no writer is provided, the output is returned as a byte slice.

func (*FunctionComponent) RenderBuilder

func (f *FunctionComponent) RenderBuilder(buf *bytes.Buffer)

RenderBuilder writes the HTML representation directly to a buffer. Calls the function to get the actual node and renders it. Nil nodes are safely ignored.

type MemoisedNode added in v0.2.2

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

MemoisedNode wraps a lazy closure with a cache key. It satisfies Node so it slots into any position in a render tree, and Memoiser so the diff engine's memoisation layer can inspect the key.

func Memoise added in v0.2.2

func Memoise(key any, fn func() Node) *MemoisedNode

Memoise creates a lazy node with a cache key. The closure produces the subtree on demand. When used with a plain Differ, the closure is called unconditionally on every render (memoised nodes are transparent). When used with fluent-jit's Memoiser wrapper, the closure is skipped if the key matches the previous render at the same tree position.

The key is compared with ==. Use a version counter, a comparable struct, or any value where equality means "the subtree has not changed". Slices, maps, and functions are not comparable and will panic.

node.Memoise(s.ItemsVersion, func() node.Node {
    return renderTable(s.Items)
})

func (*MemoisedNode) MemoiseKey added in v0.2.3

func (m *MemoisedNode) MemoiseKey() any

MemoiseKey returns the cache key for this node. The memoisation layer compares this with the previous render's key at the same position.

func (*MemoisedNode) MemoiseRender added in v0.2.3

func (m *MemoisedNode) MemoiseRender() Node

MemoiseRender calls the closure and returns the resulting subtree. The memoisation layer calls this only when the key does not match the previous render (cache miss).

func (*MemoisedNode) Nodes added in v0.2.2

func (m *MemoisedNode) Nodes() []Node

Nodes calls the closure and returns its output so tree walkers see the same children that Render produces.

func (*MemoisedNode) Render added in v0.2.2

func (m *MemoisedNode) Render(w ...io.Writer) []byte

Render calls the closure unconditionally and renders the result. This is the path taken by a plain Differ (which does not check for Memoiser). The closure always executes - no key checking.

func (*MemoisedNode) RenderBuilder added in v0.2.2

func (m *MemoisedNode) RenderBuilder(buf *bytes.Buffer)

RenderBuilder calls the closure and writes the result into the buffer. Nil closures and nil returns render nothing.

type Memoiser added in v0.2.2

type Memoiser interface {
	MemoiseKey() any
	MemoiseRender() Node
}

Memoiser is satisfied by nodes that carry a cache key for the diff engine's memoisation layer. When the key matches the previous render at the same tree position, the subtree is skipped entirely - the closure never runs and no diff is computed for that region.

Regular Diff does not check for this interface. Only fluent-jit's [jit.Memoiser] struct inspects it. When Diff encounters a memoised node, it calls Render/RenderBuilder/Nodes like any other node (the closure executes unconditionally). This makes memoised nodes transparent to code that does not use memoisation.

type Node

type Node interface {
	// Render returns the HTML as a byte slice, or writes it to the provided writer.
	// Use this for top-level rendering where you need the final output.
	//
	// Write errors are deliberately discarded rather than returned. A write
	// failure during rendering is almost always a disconnected client or a
	// closed stream - a condition the render tree cannot act on. The real
	// error path lives with the writer's owner (the HTTP handler, the buffer
	// consumer), not inside rendering. Returning an error here would force
	// every node in the tree to handle a failure that isn't its to handle.
	// Implementations silently drop write errors on purpose.
	Render(w ...io.Writer) []byte

	// RenderBuilder writes HTML into a shared buffer to avoid allocations
	// when composing a tree of nodes. Parent nodes call this on their children.
	RenderBuilder(*bytes.Buffer)

	// Nodes returns the children that will be rendered. For conditionals
	// this is the active branch only; for function components this is the
	// evaluated output. Tree walkers rely on this matching Render output.
	Nodes() []Node
}

Node represents any renderable item in an HTML document tree. This is the base contract that all renderable types satisfy - text, elements, function components, and conditionals alike.

Jump to

Keyboard shortcuts

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