dom API

dom

package

API reference for the dom package.

F
function

TestNullElementIsInert

A null element (missing query result) must be inert: mutators no-op and
readers return zero values instead of panicking.

Parameters

dom/element_null_test.go:9-23
func TestNullElementIsInert(t *testing.T)

{
	el := Doc().Query("#does-not-exist")
	el.SetHTML("<b>x</b>")
	el.SetText("x")
	el.SetAttr("a", "b")
	el.SetStyle("color", "red")
	el.SetValue("v")
	el.AddClass("c")
	el.RemoveClass("c")
	el.ToggleClass("c")
	if el.Text() != "" || el.HTML() != "" || el.Val() != "" || el.Attr("a") != "" ||
		el.Checked() || el.HasClass("c") || el.Data("x") != "" {
		t.Fatalf("null element readers must return zero values")
	}
}
F
function

TestElementAttrsAndStyle

Parameters

dom/element_test.go:7-22
func TestElementAttrsAndStyle(t *testing.T)

{
	doc := Doc()
	el := doc.CreateElement("div")
	el.SetAttr("data-x", "y")
	if got := el.Attr("data-x"); got != "y" {
		t.Fatalf("Attr() = %q", got)
	}
	el.SetHTML("<span>ok</span>")
	if got := el.HTML(); got != "<span>ok</span>" {
		t.Fatalf("HTML() = %q", got)
	}
	el.SetStyle("color", "red")
	if v := el.Get("style").Call("getPropertyValue", "color").String(); v != "red" {
		t.Fatalf("style color = %q", v)
	}
}
F
function

TestElementCollections

Parameters

dom/element_test.go:24-40
func TestElementCollections(t *testing.T)

{
	doc := Doc()
	parent := doc.CreateElement("div")
	parent.SetHTML("<span>a</span><span>b</span>")
	spans := parent.QueryAll("span")
	if spans.Length() != 2 {
		t.Fatalf("Length() = %d", spans.Length())
	}
	second := spans.Index(1)
	if second.Text() != "b" {
		t.Fatalf("Index(1).Text() = %q", second.Text())
	}
	second.ToggleClass("x")
	if !second.HasClass("x") {
		t.Fatalf("ToggleClass/HasClass failed")
	}
}
F
function

TestElementAppendChild

Parameters

dom/element_test.go:42-50
func TestElementAppendChild(t *testing.T)

{
	doc := Doc()
	parent := doc.CreateElement("div")
	child := doc.CreateElement("span")
	parent.AppendChild(child)
	if got := parent.Query("span"); !got.Truthy() {
		t.Fatalf("AppendChild() did not append")
	}
}
F
function

StyleInline

StyleInline converts a map of CSS properties into an inline style string.
Keys and values are concatenated as “key:value” pairs separated by semicolons.

Parameters

styles
map[string]string

Returns

string
dom/css.go:9-22
func StyleInline(styles map[string]string) string

{
	var b strings.Builder
	first := true
	for k, v := range styles {
		if !first {
			b.WriteByte(';')
		}
		first = false
		b.WriteString(k)
		b.WriteByte(':')
		b.WriteString(v)
	}
	return b.String()
}
F
function

TestStyleInline

Parameters

dom/css_test.go:8-13
func TestStyleInline(t *testing.T)

{
	got := StyleInline(map[string]string{"color": "red", "display": "block"})
	if !strings.Contains(got, "color:red") || !strings.Contains(got, "display:block") {
		t.Fatalf("StyleInline() = %q", got)
	}
}
F
function

BindStoreInputsForComponent

BindStoreInputsForComponent is a no-op outside wasm builds.

Parameters

string
any
dom/dom_stub.go:10-10
func BindStoreInputsForComponent(string, any)

{}
F
function

BindStoreInputs

BindStoreInputs is a no-op outside wasm builds.

Parameters

any
dom/dom_stub.go:13-13
func BindStoreInputs(any)

{}
F
function

SnapshotComponentSignals

SnapshotComponentSignals is a stub returning nil outside wasm builds.

Parameters

string

Returns

map[string]any
dom/dom_stub.go:16-16
func SnapshotComponentSignals(string) map[string]any

{ return nil }
S
struct

Element

Element wraps a DOM element and provides typed helpers.

dom/element.go:8-8
type Element struct

Methods

Query
Method

Query returns the first descendant matching the CSS selector.

Parameters

sel string

Returns

func (Element) Query(sel string) Element
{
	return Element{e.Call("querySelector", sel)}
}
QueryAll
Method

QueryAll returns all descendants matching the selector.

Parameters

sel string

Returns

func (Element) QueryAll(sel string) Element
{
	return Element{e.Call("querySelectorAll", sel)}
}
ByClass
Method

ByClass returns all descendants with the given class name.

Parameters

name string

Returns

func (Element) ByClass(name string) Element
{
	return Element{e.Call("getElementsByClassName", name)}
}
ByTag
Method

ByTag returns all descendants with the given tag name.

Parameters

tag string

Returns

func (Element) ByTag(tag string) Element
{
	return Element{e.Call("getElementsByTagName", tag)}
}
Text
Method

Text returns the element's text content.

Returns

string
func (Element) Text() string
{
	if e.missing() {
		return ""
	}
	return e.Get("textContent").String()
}
SetText
Method

SetText sets the element's text content.

Parameters

txt string
func (Element) SetText(txt string)
{
	if e.missing() {
		return
	}
	e.Set("textContent", txt)
}
HTML
Method

HTML returns the element's inner HTML.

Returns

string
func (Element) HTML() string
{
	if e.missing() {
		return ""
	}
	return e.Get("innerHTML").String()
}
SetHTML
Method

SetHTML replaces the element's children with raw HTML.

Parameters

html string
func (Element) SetHTML(html string)
{
	if e.missing() {
		return
	}
	e.Set("innerHTML", html)
}
AppendChild
Method

AppendChild appends a child element.

Parameters

child Element
func (Element) AppendChild(child Element)
{
	if e.missing() {
		return
	}
	e.Call("appendChild", child.Value)
}
Attr
Method

Attr retrieves the value of an attribute or "" if unset.

Parameters

name string

Returns

string
func (Element) Attr(name string) string
{
	if e.missing() {
		return ""
	}
	v := e.Call("getAttribute", name)
	if v.Truthy() {
		return v.String()
	}
	return ""
}
SetAttr
Method

SetAttr sets the value of an attribute on the element.

Parameters

name string
value string
func (Element) SetAttr(name, value string)
{
	if e.missing() {
		return
	}
	e.Call("setAttribute", name, value)
}
RemoveAttr
Method

RemoveAttr drops an attribute from the element, the counterpart of SetAttr (needed for boolean attributes such as disabled, where an empty value still reads as set).

Parameters

name string
func (Element) RemoveAttr(name string)
{
	if e.missing() {
		return
	}
	e.Call("removeAttribute", name)
}
Matches
Method

Matches reports whether the element itself satisfies the selector, the non-walking counterpart of Closest.

Parameters

sel string

Returns

bool
func (Element) Matches(sel string) bool
{
	if e.missing() {
		return false
	}
	return e.Call("matches", sel).Bool()
}
SetStyle
Method

SetStyle sets an inline style property on the element.

Parameters

prop string
value string
func (Element) SetStyle(prop, value string)
{
	if e.missing() {
		return
	}
	e.Get("style").Call("setProperty", prop, value)
}
AddClass
Method

AddClass adds a class to the element.

Parameters

name string
func (Element) AddClass(name string)
{
	if e.missing() {
		return
	}
	e.Get("classList").Call("add", name)
}
RemoveClass
Method

RemoveClass removes a class from the element.

Parameters

name string
func (Element) RemoveClass(name string)
{
	if e.missing() {
		return
	}
	e.Get("classList").Call("remove", name)
}
HasClass
Method

HasClass reports whether the element has the given class.

Parameters

name string

Returns

bool
func (Element) HasClass(name string) bool
{
	if e.missing() {
		return false
	}
	return e.Get("classList").Call("contains", name).Bool()
}
ToggleClass
Method

ToggleClass toggles the presence of a class on the element.

Parameters

name string
func (Element) ToggleClass(name string)
{
	if e.missing() {
		return
	}
	e.Get("classList").Call("toggle", name)
}
Length
Method

Length returns the number of children when the element represents a collection.

Returns

int
func (Element) Length() int
{ return e.Get("length").Int() }
Index
Method

Index retrieves the element at the given position when representing a collection.

Parameters

i int

Returns

func (Element) Index(i int) Element
{ return Element{e.Value.Index(i)} }
Val
Method

Val returns the element's value property (inputs, selects, textareas). Named Val because the embedded js.Value field occupies Value.

Returns

string
func (Element) Val() string
{
	if e.missing() {
		return ""
	}
	return e.Get("value").String()
}
SetValue
Method

SetValue sets the element's value property.

Parameters

v string
func (Element) SetValue(v string)
{
	if e.missing() {
		return
	}
	e.Set("value", v)
}
Checked
Method

Checked reports whether a checkbox or radio input is checked.

Returns

bool
func (Element) Checked() bool
{
	if e.missing() {
		return false
	}
	return e.Get("checked").Bool()
}
Data
Method

Data reads a data-* attribute by its dataset key (camelCase: data-item-id becomes Data("itemId")).

Parameters

key string

Returns

string
func (Element) Data(key string) string
{
	if e.missing() {
		return ""
	}
	v := e.Get("dataset").Get(key)
	if !v.Truthy() {
		return ""
	}
	return v.String()
}
Closest
Method

Closest returns the nearest ancestor (or the element itself) matching the selector; check IsNull on the result for no match.

Parameters

sel string

Returns

func (Element) Closest(sel string) Element
{
	if e.missing() {
		return e
	}
	return Element{e.Call("closest", sel)}
}
missing
Method

missing reports whether the element does not exist. Query and friends return a null element instead of panicking; mutators are no-ops on it and readers return zero values, so an async callback that outlives its page (an SPA navigation while a fetch is in flight) degrades gracefully instead of killing the wasm process.

Returns

bool
func (Element) missing() bool
{ return e.IsNull() || e.IsUndefined() }
On
Method

On attaches a listener for event to the element and returns a stop function.

Parameters

event string
handler func(Event)

Returns

func()
func (Element) On(event string, handler func(Event)) func()
{
	fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		var evt js.Value
		if len(args) > 0 {
			evt = args[0]
		}
		handler(Event{evt})
		return nil
	})
	e.Call("addEventListener", event, fn)
	return func() {
		e.Call("removeEventListener", event, fn)
		fn.Release()
	}
}
OnClick
Method

OnClick attaches a click handler to the element.

Parameters

handler func(Event)

Returns

func()
func (Element) OnClick(handler func(Event)) func()
{
	return e.On("click", handler)
}
F
function

RegisterHandler

RegisterHandler registers a Go function with custom arguments in the handler registry.
If a handler with the same name already exists, the old wrapper is released.

Parameters

name
string
fn
func(this js.Value, args []js.Value) any
dom/handlers.go:28-35
func RegisterHandler(name string, fn func(this js.Value, args []js.Value) any)

{
	handlerMu.Lock()
	defer handlerMu.Unlock()
	if old, ok := handlerRegistry[name]; ok {
		old.Release()
	}
	handlerRegistry[name] = js.SafeFuncOf(fn)
}
F
function

RegisterComponentHandler

RegisterComponentHandler registers a handler owned by one component instance.

Parameters

componentID
string
name
string
fn
func(this js.Value, args []js.Value) any
dom/handlers.go:38-48
func RegisterComponentHandler(componentID, name string, fn func(this js.Value, args []js.Value) any)

{
	handlerMu.Lock()
	defer handlerMu.Unlock()
	if componentHandlerRegistry[componentID] == nil {
		componentHandlerRegistry[componentID] = make(map[string]js.Func)
	}
	if old, ok := componentHandlerRegistry[componentID][name]; ok {
		old.Release()
	}
	componentHandlerRegistry[componentID][name] = js.SafeFuncOf(fn)
}
F
function

RegisterHandlerFunc

RegisterHandlerFunc registers a no-argument Go function in the handler registry.

Parameters

name
string
fn
func()
dom/handlers.go:51-56
func RegisterHandlerFunc(name string, fn func())

{
	RegisterHandler(name, func(_ js.Value, _ []js.Value) any {
		fn()
		return nil
	})
}
F
function

RegisterComponentHandlerFunc

RegisterComponentHandlerFunc registers a no-argument component handler.

Parameters

componentID
string
name
string
fn
func()
dom/handlers.go:59-64
func RegisterComponentHandlerFunc(componentID, name string, fn func())

{
	RegisterComponentHandler(componentID, name, func(_ js.Value, _ []js.Value) any {
		fn()
		return nil
	})
}
F
function

RegisterHandlerEvent

RegisterHandlerEvent registers a Go function that receives the first argument as an event object.

Parameters

name
string
fn
func(js.Value)
dom/handlers.go:67-76
func RegisterHandlerEvent(name string, fn func(js.Value))

{
	RegisterHandler(name, func(_ js.Value, args []js.Value) any {
		var evt js.Value
		if len(args) > 0 {
			evt = args[0]
		}
		fn(evt)
		return nil
	})
}
F
function

RegisterHandlerElem

RegisterHandlerElem registers a handler that receives the element carrying
the data-on-* attribute (resolved by event delegation, so it works for
markup injected at runtime) together with the event. This is the idiomatic
way to handle clicks on list rows: render rows with data-on-click=“name”
and read the row’s data-* attributes from el.

Parameters

name
string
fn
func(el Element, evt Event)
dom/handlers.go:83-97
func RegisterHandlerElem(name string, fn func(el Element, evt Event))

{
	RegisterHandler(name, func(_ js.Value, args []js.Value) any {
		var evt, el js.Value
		if len(args) > 0 {
			evt = args[0]
		}
		if len(args) > 1 {
			el = args[1]
		} else if evt.Truthy() {
			el = evt.Get("target")
		}
		fn(Element{el}, Event{evt})
		return nil
	})
}
F
function

GetHandler

GetHandler retrieves a registered handler by name.

Parameters

name
string

Returns

dom/handlers.go:100-107
func GetHandler(name string) js.Func

{
	handlerMu.RLock()
	defer handlerMu.RUnlock()
	if v, ok := handlerRegistry[name]; ok {
		return v
	}
	return js.Func{}
}
F
function

GetComponentHandler

GetComponentHandler resolves a component handler before the global fallback.

Parameters

componentID
string
name
string

Returns

dom/handlers.go:110-119
func GetComponentHandler(componentID, name string) js.Func

{
	handlerMu.RLock()
	defer handlerMu.RUnlock()
	if handlers := componentHandlerRegistry[componentID]; handlers != nil {
		if v, ok := handlers[name]; ok {
			return v
		}
	}
	return handlerRegistry[name]
}
F
function

ReleaseComponentHandlers

ReleaseComponentHandlers releases every handler owned by a component.

Parameters

componentID
string
dom/handlers.go:122-130
func ReleaseComponentHandlers(componentID string)

{
	handlerMu.Lock()
	handlers := componentHandlerRegistry[componentID]
	delete(componentHandlerRegistry, componentID)
	handlerMu.Unlock()
	for _, handler := range handlers {
		handler.Release()
	}
}
S
struct

delegatedHandler

dom/handlers.go:132-137
type delegatedHandler struct

Fields

Name Type Description
event string
capture bool
fn js.Func
stop func()
F
function

DelegateEvents

DelegateEvents attaches delegated event listeners on the component root
element. Bubbling events bubble up to root where data-on-* attributes
are resolved to registered handlers.

Delegating twice for the same component (a remount, a root replaced by a
re-render of the surrounding markup) replaces the previous set: keeping it
would fire every handler twice and leak one js.Func per event per remount.

Parameters

componentID
string
root
dom/handlers.go:146-164
func DelegateEvents(componentID string, root js.Value)

{
	RemoveDelegatedEvents(componentID, root)

	var handlers []delegatedHandler
	events := []string{"click", "submit", "input", "change", "keydown", "keyup", "focus", "blur"}
	for _, evtName := range events {
		for _, capture := range []bool{false, true} {
			if (evtName == "focus" || evtName == "blur") && !capture {
				continue
			}
			handler := newDelegatedHandler(componentID, root, evtName, capture)
			handlers = append(handlers, handler)
			root.Call("addEventListener", evtName, handler.fn, capture)
		}
	}
	delegateMu.Lock()
	delegates[componentID] = handlers
	delegateMu.Unlock()
}
F
function

newDelegatedHandler

Parameters

componentID
string
root
event
string
capture
bool
dom/handlers.go:166-259
func newDelegatedHandler(componentID string, root js.Value, event string, capture bool) delegatedHandler

{
	var timerMu sync.Mutex
	timers := make(map[string]*time.Timer)
	throttled := make(map[string]time.Time)

	fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
		if len(args) == 0 {
			return nil
		}
		evt := args[0]
		target := evt.Get("target")
		for target.Truthy() {
			handlerName := target.Call("getAttribute", "data-on-"+event)
			if handlerName.Truthy() {
				modifiers := eventModifiers(target, event)
				_, wantsCapture := modifiers["capture"]
				nonBubbling := event == "focus" || event == "blur"
				if (nonBubbling || wantsCapture == capture) && eventAllowed(evt, target, modifiers) {
					h := GetComponentHandler(componentID, handlerName.String())
					if h.Truthy() {
						if _, ok := modifiers["prevent"]; ok {
							if _, passive := modifiers["passive"]; !passive {
								evt.Call("preventDefault")
							}
						}
						if _, ok := modifiers["stop"]; ok {
							evt.Call("stopPropagation")
						}
						if _, ok := modifiers["once"]; ok {
							target.Call("removeAttribute", "data-on-"+event)
							target.Call("removeAttribute", "data-on-"+event+"-modifiers")
						}

						invoke := func() {
							defer func() {
								if r := recover(); r != nil && OnHandlerPanic != nil {
									OnHandlerPanic(r, handlerName.String())
								}
							}()
							h.Invoke(evt, target)
						}
						key := eventBindingKey(target, event, handlerName.String())
						if delay, ok := modifierDelay(modifiers, "debounce"); ok {
							timerMu.Lock()
							if timer := timers[key]; timer != nil {
								timer.Stop()
							}
							var scheduled *time.Timer
							scheduled = time.AfterFunc(delay, func() {
								invoke()
								timerMu.Lock()
								if timers[key] == scheduled {
									delete(timers, key)
								}
								timerMu.Unlock()
							})
							timers[key] = scheduled
							timerMu.Unlock()
							return nil
						}
						if delay, ok := modifierDelay(modifiers, "throttle"); ok {
							timerMu.Lock()
							last := throttled[key]
							if time.Since(last) < delay {
								timerMu.Unlock()
								return nil
							}
							throttled[key] = time.Now()
							timerMu.Unlock()
						}
						invoke()
						return nil
					}
				}
			}
			if target.Equal(root) {
				break
			}
			target = target.Get("parentElement")
		}
		return nil
	})

	stop := func() {
		timerMu.Lock()
		for _, timer := range timers {
			timer.Stop()
		}
		clear(timers)
		clear(throttled)
		timerMu.Unlock()
	}
	return delegatedHandler{event: event, capture: capture, fn: fn, stop: stop}
}
F
function

eventModifiers

Parameters

target
event
string

Returns

map[string]struct{}
dom/handlers.go:261-274
func eventModifiers(target js.Value, event string) map[string]struct{}

{
	raw := target.Call("getAttribute", "data-on-"+event+"-modifiers")
	modifiers := make(map[string]struct{})
	if !raw.Truthy() {
		return modifiers
	}
	for _, modifier := range strings.Split(raw.String(), ",") {
		modifier = strings.ToLower(strings.TrimSpace(modifier))
		if modifier != "" {
			modifiers[modifier] = struct{}{}
		}
	}
	return modifiers
}
F
function

eventAllowed

Parameters

evt
target
modifiers
map[string]struct{}

Returns

bool
dom/handlers.go:276-304
func eventAllowed(evt, target js.Value, modifiers map[string]struct{}) bool

{
	if _, ok := modifiers["self"]; ok && !evt.Get("target").Equal(target) {
		return false
	}
	keys := map[string]string{
		"enter": "Enter", "escape": "Escape", "tab": "Tab", "space": " ",
		"up": "ArrowUp", "down": "ArrowDown", "left": "ArrowLeft", "right": "ArrowRight",
	}
	for modifier, key := range keys {
		if _, ok := modifiers[modifier]; ok && evt.Get("key").String() != key {
			return false
		}
	}
	system := map[string]string{"ctrl": "ctrlKey", "shift": "shiftKey", "alt": "altKey", "meta": "metaKey"}
	for modifier, property := range system {
		if _, ok := modifiers[modifier]; ok && !evt.Get(property).Bool() {
			return false
		}
	}
	if _, exact := modifiers["exact"]; exact {
		for modifier, property := range system {
			_, required := modifiers[modifier]
			if evt.Get(property).Bool() != required {
				return false
			}
		}
	}
	return true
}
F
function

modifierDelay

Parameters

modifiers
map[string]struct{}
name
string

Returns

dom/handlers.go:306-318
func modifierDelay(modifiers map[string]struct{}, name string) (time.Duration, bool)

{
	if _, ok := modifiers[name]; !ok {
		return 0, false
	}
	delay := 300
	for modifier := range modifiers {
		if ms, err := strconv.Atoi(modifier); err == nil && ms >= 0 {
			delay = ms
			break
		}
	}
	return time.Duration(delay) * time.Millisecond, true
}
F
function

eventBindingKey

Parameters

target
event
string
handler
string

Returns

string
dom/handlers.go:320-329
func eventBindingKey(target js.Value, event, handler string) string

{
	const property = "__rfwEventBinding"
	id := target.Get(property)
	if !id.Truthy() {
		value := strconv.FormatUint(eventBindingSeq.Add(1), 10)
		target.Set(property, value)
		id = target.Get(property)
	}
	return id.String() + ":" + event + ":" + handler
}
F
function

RemoveDelegatedEvents

RemoveDelegatedEvents removes all delegated event listeners for the given component.

Parameters

componentID
string
root
dom/handlers.go:332-352
func RemoveDelegatedEvents(componentID string, root js.Value)

{
	delegateMu.Lock()
	handlers, ok := delegates[componentID]
	if ok {
		delete(delegates, componentID)
	}
	delegateMu.Unlock()
	if !ok {
		return
	}
	// A root that is already gone (its subtree was replaced) cannot have its
	// listeners detached, but the callbacks still have to be released.
	live := root.Truthy()
	for _, handler := range handlers {
		if live {
			root.Call("removeEventListener", handler.event, handler.fn.Value, handler.capture)
		}
		handler.stop()
		handler.fn.Release()
	}
}
S
struct

Document

Document wraps the global document object.

dom/document.go:8-8
type Document struct

Methods

ByID
Method

ByID fetches an element by id.

Parameters

id string

Returns

func (Document) ByID(id string) Element
{
	if !d.Truthy() {
		return Element{js.Null()}
	}
	return Element{d.Call("getElementById", id)}
}
Query
Method

Query returns the first element matching the selector.

Parameters

sel string

Returns

func (Document) Query(sel string) Element
{
	if !d.Truthy() {
		return Element{js.Null()}
	}
	return Element{d.Call("querySelector", sel)}
}
QueryAll
Method

QueryAll returns all elements matching the selector.

Parameters

sel string

Returns

func (Document) QueryAll(sel string) Element
{
	if !d.Truthy() {
		return Element{js.Null()}
	}
	return Element{d.Call("querySelectorAll", sel)}
}
ByClass
Method

ByClass returns all elements with the given class name.

Parameters

name string

Returns

func (Document) ByClass(name string) Element
{
	return Element{d.Call("getElementsByClassName", name)}
}
ByTag
Method

ByTag returns all elements with the given tag name.

Parameters

tag string

Returns

func (Document) ByTag(tag string) Element
{
	return Element{d.Call("getElementsByTagName", tag)}
}
CreateElement
Method

CreateElement creates a new element with the tag.

Parameters

tag string

Returns

func (Document) CreateElement(tag string) Element
{
	return Element{d.Call("createElement", tag)}
}
Body
Method

Body returns the document's <body> element.

Returns

func (Document) Body() Element
{ return Element{d.Get("body")} }
F
function

Doc

Doc returns the global Document.

Returns

dom/document.go:11-11
func Doc() Document

{ return Document{js.Doc()} }
F
function

addInputBindingStop

Parameters

componentID
string
stop
func()
dom/dom.go:32-36
func addInputBindingStop(componentID string, stop func())

{
	inputBindingStopsMu.Lock()
	inputBindingStops[componentID] = append(inputBindingStops[componentID], stop)
	inputBindingStopsMu.Unlock()
}
F
function

ReleaseInputBindings

ReleaseInputBindings stops all input listeners registered for a component.
UpdateDOM calls it before rebinding and core calls it on unmount.

Parameters

componentID
string
dom/dom.go:40-48
func ReleaseInputBindings(componentID string)

{
	inputBindingStopsMu.Lock()
	stops := inputBindingStops[componentID]
	delete(inputBindingStops, componentID)
	inputBindingStopsMu.Unlock()
	for _, stop := range stops {
		stop()
	}
}
F
function

RegisterSignal

RegisterSignal associates a signal with a component so inputs can bind to it.

Parameters

componentID
string
name
string
sig
any
dom/dom.go:56-63
func RegisterSignal(componentID, name string, sig any)

{
	componentSignalsMu.Lock()
	if componentSignals[componentID] == nil {
		componentSignals[componentID] = make(map[string]any)
	}
	componentSignals[componentID][name] = sig
	componentSignalsMu.Unlock()
}
F
function

RemoveComponentSignals

RemoveComponentSignals cleans up signals for a component on unmount.

Parameters

componentID
string
dom/dom.go:66-70
func RemoveComponentSignals(componentID string)

{
	componentSignalsMu.Lock()
	delete(componentSignals, componentID)
	componentSignalsMu.Unlock()
}
F
function

getSignal

Parameters

componentID
string
name
string

Returns

any
dom/dom.go:72-79
func getSignal(componentID, name string) any

{
	componentSignalsMu.RLock()
	defer componentSignalsMu.RUnlock()
	if m, ok := componentSignals[componentID]; ok {
		return m[name]
	}
	return nil
}
F
function

SnapshotComponentSignals

SnapshotComponentSignals returns a copy of the signals registered for a component.

Parameters

componentID
string

Returns

map[string]any
dom/dom.go:82-93
func SnapshotComponentSignals(componentID string) map[string]any

{
	componentSignalsMu.RLock()
	defer componentSignalsMu.RUnlock()
	if signals, ok := componentSignals[componentID]; ok {
		clone := make(map[string]any, len(signals))
		for k, v := range signals {
			clone[k] = v
		}
		return clone
	}
	return nil
}
F
function

ComponentRoot

ComponentRoot returns the DOM root element for a component by its ID.
Falls back to #app if id is empty or element not found.

Parameters

id
string

Returns

dom/dom.go:106-116
func ComponentRoot(id string) Element

{
	doc := Doc()
	if id == "" {
		return doc.ByID("app")
	}
	el := doc.Query(fmt.Sprintf("[data-component-id='%s']", id))
	if el.IsNull() || el.IsUndefined() {
		return doc.ByID("app")
	}
	return el
}
F
function

UpdateDOM

UpdateDOM patches the DOM of the specified component with the provided
HTML string, resolving the target via typed Document/Element wrappers.

Parameters

componentID
string
html
string
dom/dom.go:120-152
func UpdateDOM(componentID string, html string)

{
	element := ComponentRoot(componentID)
	if element.IsNull() || element.IsUndefined() {
		return
	}

	// Diff-patch only when the resolved element is the component's OWN root: that
	// is an in-place reactive update, where patching preserves focus/selection.
	// Otherwise the target is the #app fallback (a fresh mount or a route change,
	// since ComponentRoot falls back to #app when the component root is not yet
	// in the DOM). There, positionally diffing two different <root> trees leaves
	// stale nodes from the previous component, so replace wholesale instead.
	elID := element.Call("getAttribute", "data-component-id")
	if componentID != "" && elID.Truthy() && elID.String() == componentID {
		patchInnerHTML(element.Value, html)
	} else {
		element.Set("innerHTML", html)
	}

	if TemplateHook != nil {
		TemplateHook(componentID, html)
	}

	// Release the listeners of the previous render: rebinding below attaches
	// fresh ones and stale listeners on replaced nodes would leak.
	ReleaseInputBindings(componentID)

	BindStoreInputsForComponent(componentID, element.Value)
	BindSignalInputs(componentID, element.Value)
	BindASTStoreInputs(componentID, element.Value)
	BindASTSignalInputs(componentID, element.Value)
	UpdateLifecycleHooks(componentID)
}
F
function

UpdateMountedDOM

UpdateMountedDOM patches a component’s subtree only when its own root is in
the DOM. Reactive updates (store/signal changes) go through here: a change
hitting a component that is not mounted yet (a constructor-time Set) or not
anymore must be a no-op, not a wholesale replacement of the #app fallback.

Parameters

componentID
string
html
string
dom/dom.go:158-168
func UpdateMountedDOM(componentID, html string)

{
	el := ComponentRoot(componentID)
	if el.IsNull() || el.IsUndefined() {
		return
	}
	id := el.Call("getAttribute", "data-component-id")
	if !id.Truthy() || id.String() != componentID {
		return
	}
	UpdateDOM(componentID, html)
}
F
function

UpdateDOMIn

UpdateDOMIn renders html into an explicit target element (the router
outlet). The subtree is replaced wholesale: across different component
trees a positional diff would leave stale nodes behind.

Parameters

target
componentID
string
html
string
dom/dom.go:173-190
func UpdateDOMIn(target Element, componentID, html string)

{
	if target.IsNull() || target.IsUndefined() {
		return
	}
	target.Set("innerHTML", html)

	if TemplateHook != nil {
		TemplateHook(componentID, html)
	}

	ReleaseInputBindings(componentID)

	BindStoreInputsForComponent(componentID, target.Value)
	BindSignalInputs(componentID, target.Value)
	BindASTStoreInputs(componentID, target.Value)
	BindASTSignalInputs(componentID, target.Value)
	UpdateLifecycleHooks(componentID)
}
F
function

BindASTStoreInputs

BindASTStoreInputs binds input elements that have data-bind-store attributes
(emitted by the AST renderer) to their store variables.

Parameters

componentID
string
element
dom/dom.go:194-241
func BindASTStoreInputs(componentID string, element js.Value)

{
	inputs := element.Call("querySelectorAll", "[data-bind-store]")
	for i := 0; i < inputs.Length(); i++ {
		input := inputs.Index(i)
		binding := input.Call("getAttribute", "data-bind-store").String()
		parts := strings.Split(binding, ".")
		if len(parts) != 3 {
			continue
		}
		module, storeName, key := parts[0], parts[1], parts[2]
		store := state.GlobalStoreManager.GetStore(module, storeName)
		if store == nil {
			continue
		}
		if StoreBindingHook != nil && componentID != "" {
			StoreBindingHook(componentID, module, storeName, key)
		}
		storeValue := store.Get(key)
		tag := strings.ToLower(input.Get("tagName").String())
		if tag == "input" {
			inputType := input.Get("type").String()
			if inputType == "checkbox" {
				if b, ok := storeValue.(bool); ok {
					input.Set("checked", b)
				}
				ch, stop := events.Listen("change", input)
				addInputBindingStop(componentID, stop)
				go func(in js.Value, st *state.Store, k string) {
					for range ch {
						st.Set(k, in.Get("checked").Bool())
					}
				}(input, store, key)
				continue
			}
		}
		if storeValue == nil {
			storeValue = ""
		}
		input.Set("value", fmt.Sprintf("%v", storeValue))
		ch, stop := events.Listen("input", input)
		addInputBindingStop(componentID, stop)
		go func(in js.Value, st *state.Store, k string) {
			for range ch {
				st.Set(k, in.Get("value").String())
			}
		}(input, store, key)
	}
}
F
function

BindASTSignalInputs

BindASTSignalInputs binds input elements that have data-bind-signal attributes
(emitted by the AST renderer) to their signals.

Parameters

componentID
string
element
dom/dom.go:245-290
func BindASTSignalInputs(componentID string, element js.Value)

{
	inputs := element.Call("querySelectorAll", "[data-bind-signal]")
	for i := 0; i < inputs.Length(); i++ {
		input := inputs.Index(i)
		name := input.Call("getAttribute", "data-bind-signal").String()
		sig := getSignal(componentID, name)
		if sig == nil {
			continue
		}
		tag := strings.ToLower(input.Get("tagName").String())
		if tag == "input" {
			inputType := input.Get("type").String()
			if inputType == "checkbox" {
				if s, ok := sig.(interface {
					Read() any
					Set(bool)
				}); ok {
					if b, ok := s.Read().(bool); ok {
						input.Set("checked", b)
					}
					ch, stop := events.Listen("change", input)
					addInputBindingStop(componentID, stop)
					go func(in js.Value, sg interface{ Set(bool) }) {
						for range ch {
							sg.Set(in.Get("checked").Bool())
						}
					}(input, s)
					continue
				}
			}
		}
		if s, ok := sig.(interface {
			Read() any
			Set(string)
		}); ok {
			input.Set("value", fmt.Sprintf("%v", s.Read()))
			ch, stop := events.Listen("input", input)
			addInputBindingStop(componentID, stop)
			go func(in js.Value, sg interface{ Set(string) }) {
				for range ch {
					sg.Set(in.Get("value").String())
				}
			}(input, s)
		}
	}
}
F
function

BindStoreInputsForComponent

BindStoreInputsForComponent binds input elements to store variables while
providing the component context for runtime hooks.

Parameters

componentID
string
element
dom/dom.go:294-359
func BindStoreInputsForComponent(componentID string, element js.Value)

{
	inputs := element.Call("querySelectorAll", "input, select, textarea")
	for i := 0; i < inputs.Length(); i++ {
		input := inputs.Index(i)

		valueAttr := ""
		if input.Call("hasAttribute", "value").Bool() {
			valueAttr = input.Call("getAttribute", "value").String()
		}
		checkedAttr := ""
		if input.Call("hasAttribute", "checked").Bool() {
			checkedAttr = input.Call("getAttribute", "checked").String()
		}

		re := reStoreWrite
		valueMatch := re.FindStringSubmatch(valueAttr)
		checkedMatch := re.FindStringSubmatch(checkedAttr)

		var module, storeName, key string
		var usesChecked bool
		if len(valueMatch) == 4 {
			module, storeName, key = valueMatch[1], valueMatch[2], valueMatch[3]
		} else if len(checkedMatch) == 4 {
			module, storeName, key = checkedMatch[1], checkedMatch[2], checkedMatch[3]
			usesChecked = true
		} else {
			continue
		}

		store := state.GlobalStoreManager.GetStore(module, storeName)
		if store == nil {
			continue
		}

		if StoreBindingHook != nil && componentID != "" {
			StoreBindingHook(componentID, module, storeName, key)
		}

		storeValue := store.Get(key)

		if usesChecked {
			boolVal, _ := storeValue.(bool)
			input.Set("checked", boolVal)
			ch, stop := events.Listen("change", input)
			addInputBindingStop(componentID, stop)
			go func(in js.Value, st *state.Store, k string) {
				for range ch {
					st.Set(k, in.Get("checked").Bool())
				}
			}(input, store, key)
			continue
		}

		if storeValue == nil {
			storeValue = ""
		}
		input.Set("value", fmt.Sprintf("%v", storeValue))
		ch, stop := events.Listen("input", input)
		addInputBindingStop(componentID, stop)
		go func(in js.Value, st *state.Store, k string) {
			for range ch {
				st.Set(k, in.Get("value").String())
			}
		}(input, store, key)
	}
}
F
function

BindStoreInputs

BindStoreInputs binds input elements to store variables.

Parameters

element
dom/dom.go:362-364
func BindStoreInputs(element js.Value)

{
	BindStoreInputsForComponent("", element)
}
F
function

BindSignalInputs

BindSignalInputs binds input elements to local component signals.

Parameters

componentID
string
element
dom/dom.go:367-434
func BindSignalInputs(componentID string, element js.Value)

{
	inputs := element.Call("querySelectorAll", "input, select, textarea")
	for i := 0; i < inputs.Length(); i++ {
		input := inputs.Index(i)

		valueAttr := ""
		if input.Call("hasAttribute", "value").Bool() {
			valueAttr = input.Call("getAttribute", "value").String()
		}
		checkedAttr := ""
		if input.Call("hasAttribute", "checked").Bool() {
			checkedAttr = input.Call("getAttribute", "checked").String()
		}

		re := reSignalWrite
		valueMatch := re.FindStringSubmatch(valueAttr)
		checkedMatch := re.FindStringSubmatch(checkedAttr)

		var name string
		var usesChecked bool
		if len(valueMatch) == 2 {
			name = valueMatch[1]
		} else if len(checkedMatch) == 2 {
			name = checkedMatch[1]
			usesChecked = true
		} else {
			continue
		}

		sig := getSignal(componentID, name)
		if sig == nil {
			continue
		}

		if usesChecked {
			if s, ok := sig.(interface {
				Read() any
				Set(bool)
			}); ok {
				if b, ok := s.Read().(bool); ok {
					input.Set("checked", b)
				}
				ch, stop := events.Listen("change", input)
				addInputBindingStop(componentID, stop)
				go func(in js.Value, sg interface{ Set(bool) }) {
					for range ch {
						sg.Set(in.Get("checked").Bool())
					}
				}(input, s)
			}
			continue
		}

		if s, ok := sig.(interface {
			Read() any
			Set(string)
		}); ok {
			input.Set("value", fmt.Sprintf("%v", s.Read()))
			ch, stop := events.Listen("input", input)
			addInputBindingStop(componentID, stop)
			go func(in js.Value, sg interface{ Set(string) }) {
				for range ch {
					sg.Set(in.Get("value").String())
				}
			}(input, s)
		}
	}
}
F
function

patchInnerHTML

Parameters

element
html
string
dom/dom.go:436-479
func patchInnerHTML(element js.Value, html string)

{
	activeEl := js.Global().Get("document").Get("activeElement")
	activeID := ""
	activeSelStart := 0
	activeSelEnd := 0
	if activeEl.Truthy() {
		tag := activeEl.Get("nodeName").String()
		if tag == "INPUT" || tag == "TEXTAREA" || tag == "SELECT" {
			activeID = activeEl.Get("id").String()
			if activeID != "" {
				activeSelStart = activeEl.Get("selectionStart").Int()
				activeSelEnd = activeEl.Get("selectionEnd").Int()
			}
		}
	}

	template := CreateElement("template")
	template.Set("innerHTML", html)
	newContent := template.Get("content")

	patched := false
	firstChild := newContent.Get("firstChild")
	if firstChild.Truthy() && firstChild.Get("nodeName").String() == "ROOT" {
		cid := element.Call("getAttribute", "data-component-id")
		newCid := firstChild.Call("getAttribute", "data-component-id")
		if cid.Truthy() && cid.String() == newCid.String() {
			patchAttributes(element, firstChild)
			patchChildren(element, firstChild)
			patched = true
		}
	}

	if !patched {
		patchChildren(element, newContent)
	}

	if activeID != "" {
		restore := js.Global().Get("document").Call("getElementById", activeID)
		if restore.Truthy() {
			restore.Call("focus")
			restore.Call("setSelectionRange", activeSelStart, activeSelEnd)
		}
	}
}
F
function

patchChildren

Parameters

oldParent
newParent
dom/dom.go:481-564
func patchChildren(oldParent, newParent js.Value)

{
	// Snapshot the significant children (elements and non-blank text).
	// Whitespace-only text nodes are formatting noise: pairing them
	// positionally shifts the diff whenever a keyed list grows or a
	// conditional toggles, morphing unrelated siblings into each other.
	oldKids := significantChildren(oldParent)
	newKids := significantChildren(newParent)

	keyed := make(map[string]js.Value)
	for _, child := range oldKids {
		if key := getDataKey(child); key != "" {
			keyed[key] = child
		}
	}

	consumed := make([]bool, len(oldKids))
	oi := 0
	// cursor returns the first unconsumed old node: inserts anchor before it.
	cursor := func() js.Value {
		for i := oi; i < len(oldKids); i++ {
			if !consumed[i] {
				return oldKids[i]
			}
		}
		return js.Null()
	}
	insertAtCursor := func(node js.Value) {
		if ref := cursor(); ref.Truthy() {
			oldParent.Call("insertBefore", node, ref)
		} else {
			oldParent.Call("appendChild", node)
		}
	}

	for _, newChild := range newKids {
		if key := getDataKey(newChild); key != "" {
			if oldChild, ok := keyed[key]; ok {
				patchNode(oldChild, newChild)
				if ref := cursor(); !oldChild.Equal(ref) {
					insertAtCursor(oldChild)
				} else {
					// already in position: consume it
					for i := oi; i < len(oldKids); i++ {
						if oldKids[i].Equal(oldChild) {
							consumed[i] = true
							break
						}
					}
				}
				delete(keyed, key)
			} else {
				insertAtCursor(newChild.Call("cloneNode", true))
			}
			continue
		}

		// advance past keyed leftovers (handled through the map above)
		for oi < len(oldKids) && (consumed[oi] || getDataKey(oldKids[oi]) != "") {
			oi++
		}
		if oi < len(oldKids) && samePatchType(oldKids[oi], newChild) {
			patchNode(oldKids[oi], newChild)
			consumed[oi] = true
			oi++
		} else if oi < len(oldKids) {
			oldParent.Call("replaceChild", newChild.Call("cloneNode", true), oldKids[oi])
			consumed[oi] = true
			oi++
		} else {
			oldParent.Call("appendChild", newChild.Call("cloneNode", true))
		}
	}

	// leftover keyed nodes not reused by the new render
	for _, child := range keyed {
		child.Call("remove")
	}
	// leftover unkeyed significant nodes past the new list
	for i := 0; i < len(oldKids); i++ {
		if !consumed[i] && getDataKey(oldKids[i]) == "" {
			oldKids[i].Call("remove")
		}
	}
}
F
function

significantChildren

significantChildren returns the child nodes that participate in diffing:
elements and text nodes with non-whitespace content.

Parameters

parent

Returns

dom/dom.go:568-579
func significantChildren(parent js.Value) []js.Value

{
	children := parent.Get("childNodes")
	out := make([]js.Value, 0, children.Length())
	for i := 0; i < children.Length(); i++ {
		child := children.Index(i)
		if child.Get("nodeType").Int() == 3 && strings.TrimSpace(child.Get("nodeValue").String()) == "" {
			continue
		}
		out = append(out, child)
	}
	return out
}
F
function

samePatchType

samePatchType reports whether two nodes may be patched in place: same node
name and, for conditional wrappers, the same data-condition identity (a
wrapper morphing into an unrelated sibling emptied whole sections).

Parameters

oldNode
newNode

Returns

bool
dom/dom.go:584-601
func samePatchType(oldNode, newNode js.Value) bool

{
	if oldNode.Get("nodeName").String() != newNode.Get("nodeName").String() {
		return false
	}
	if oldNode.Get("nodeType").Int() != 1 {
		return true
	}
	oc := oldNode.Call("getAttribute", "data-condition")
	nc := newNode.Call("getAttribute", "data-condition")
	os, ns := "", ""
	if !oc.IsNull() {
		os = oc.String()
	}
	if !nc.IsNull() {
		ns = nc.String()
	}
	return os == ns
}
F
function

getDataKey

Parameters

node

Returns

string
dom/dom.go:603-612
func getDataKey(node js.Value) string

{
	if node.Get("nodeType").Int() != 1 {
		return ""
	}
	key := node.Call("getAttribute", "data-key")
	if key.Truthy() {
		return key.String()
	}
	return ""
}
F
function

patchNode

Parameters

oldNode
newNode
dom/dom.go:614-641
func patchNode(oldNode, newNode js.Value)

{
	nodeType := newNode.Get("nodeType").Int()

	// The router owns whatever sits inside an outlet. A shell that re-renders
	// (a store-driven list or condition in a MountRoot root) must not diff the
	// routed page away, and must not undo the DOM a mounted page built for
	// itself after its own render.
	if nodeType == 1 && isRouterOutlet(oldNode) && isRouterOutlet(newNode) {
		patchAttributes(oldNode, newNode)
		return
	}
	if nodeType == 3 { // Text node
		if oldNode.Get("nodeValue").String() != newNode.Get("nodeValue").String() {
			oldNode.Set("nodeValue", newNode.Get("nodeValue"))
		}
		return
	}

	if oldNode.Get("nodeName").String() != newNode.Get("nodeName").String() {
		oldNode.Call("replaceWith", newNode.Call("cloneNode", true))
		return
	}

	if nodeType == 1 { // Element node
		patchAttributes(oldNode, newNode)
	}
	patchChildren(oldNode, newNode)
}
F
function

isRouterOutlet

isRouterOutlet reports whether the node is the router’s outlet marker.

Parameters

node

Returns

bool
dom/dom.go:644-649
func isRouterOutlet(node js.Value) bool

{
	if node.Get("nodeType").Int() != 1 {
		return false
	}
	return node.Call("hasAttribute", "data-router-outlet").Bool()
}
F
function

patchAttributes

Parameters

oldNode
newNode
dom/dom.go:651-668
func patchAttributes(oldNode, newNode js.Value)

{
	oldAttrs := oldNode.Call("getAttributeNames")
	for i := 0; i < oldAttrs.Length(); i++ {
		name := oldAttrs.Index(i).String()
		if !newNode.Call("hasAttribute", name).Bool() {
			oldNode.Call("removeAttribute", name)
		}
	}

	newAttrs := newNode.Call("getAttributeNames")
	for i := 0; i < newAttrs.Length(); i++ {
		name := newAttrs.Index(i).String()
		val := newNode.Call("getAttribute", name)
		if oldNode.Call("getAttribute", name).String() != val.String() {
			oldNode.Call("setAttribute", name, val)
		}
	}
}
F
function

TestUpdateDOMSkipsNonElementNodes

Ensure UpdateDOM handles nodes without attributes (e.g. comments) without panicking.

Parameters

dom/dom_patch_test.go:12-21
func TestUpdateDOMSkipsNonElementNodes(_ *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("div")
	root.Set("id", "root")
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	SetInnerHTML(root, "<!--old-->")
	UpdateDOM("root", "<!--new-->")
}
S
struct

Event

Event wraps a browser event.

dom/event.go:8-8
type Event struct

Methods

PreventDefault prevents the default action for the event.

func (Event) PreventDefault()
{ e.Call("preventDefault") }

StopPropagation stops the event from bubbling.

func (Event) StopPropagation()
{ e.Call("stopPropagation") }
F
function

ExpandEvents

ExpandEvents rewrites @on:event:handler directives into the data-on-*
attributes event delegation resolves. Templates go through it
automatically; call it on markup built at runtime so dynamic rows can use
the same syntax as .rtml files:

rows += <tr @on:click:openRow data-id=" + id + ">...</tr>
el.SetHTML(dom.ExpandEvents(rows))

Parameters

markup
string

Returns

string
dom/markup.go:21-38
func ExpandEvents(markup string) string

{
	return reExpandEvent.ReplaceAllStringFunc(markup, func(match string) string {
		parts := reExpandEvent.FindStringSubmatch(match)
		if len(parts) != 5 {
			return match
		}
		fullEvent := parts[2]
		handler := parts[3]
		suffix := parts[4]
		eventParts := strings.Split(fullEvent, ".")
		event := eventParts[0]
		attr := fmt.Sprintf("data-on-%s=%q", event, handler)
		if len(eventParts) > 1 {
			attr += fmt.Sprintf(" data-on-%s-modifiers=%q", event, strings.Join(eventParts[1:], ","))
		}
		return attr + suffix
	})
}
F
function

ScheduleRender

ScheduleRender updates the DOM of the specified component after a delay.

Parameters

componentID
string
html
string
dom/schedule.go:16-28
func ScheduleRender(componentID string, html string, delay time.Duration)

{
	sched.Lock()
	defer sched.Unlock()
	if t, ok := sched.timers[componentID]; ok {
		t.Stop()
	}
	sched.timers[componentID] = time.AfterFunc(delay, func() {
		UpdateDOM(componentID, html)
		sched.Lock()
		delete(sched.timers, componentID)
		sched.Unlock()
	})
}
S
struct

binding

binding represents a precompiled event binding.

dom/bindings.go:14-19
type binding struct

Fields

Name Type Description
Path []int
Event string
Handler string
Modifiers []string
F
function

RegisterBindings

RegisterBindings generates and associates bindings for a component instance.

Parameters

id
string
name
string
template
string
dom/bindings.go:29-40
func RegisterBindings(id, name, template string)

{
	if bs, ok := precompiledByName[name]; ok {
		compiledBindings[id] = bs
		return
	}
	bs, err := parseTemplate(template)
	if err != nil {
		return
	}
	precompiledByName[name] = bs
	compiledBindings[id] = bs
}
F
function

OverrideBindings

OverrideBindings replaces the cached bindings for a component name.

Parameters

name
string
template
string
dom/bindings.go:43-49
func OverrideBindings(name, template string)

{
	bs, err := parseTemplate(template)
	if err != nil {
		return
	}
	precompiledByName[name] = bs
}
F
function

parseTemplate

Parameters

tpl
string

Returns

error
dom/bindings.go:51-58
func parseTemplate(tpl string) ([]binding, error)

{
	processed := replaceEventHandlers(tpl)
	node, err := html.Parse(strings.NewReader(processed))
	if err != nil {
		return nil, err
	}
	return collectBindings(node, nil), nil
}
F
function

collectBindings

Parameters

path
[]int

Returns

dom/bindings.go:60-91
func collectBindings(n *html.Node, path []int) []binding

{
	var res []binding
	if n.Type == html.ElementNode {
		attrs := map[string]string{}
		for _, a := range n.Attr {
			attrs[a.Key] = a.Val
		}
		for k, v := range attrs {
			if strings.HasPrefix(k, "data-on-") && !strings.HasSuffix(k, "-modifiers") {
				event := strings.TrimPrefix(k, "data-on-")
				mods := []string{}
				if m, ok := attrs[fmt.Sprintf("data-on-%s-modifiers", event)]; ok && m != "" {
					for _, s := range strings.Split(m, ",") {
						s = strings.TrimSpace(s)
						if s != "" {
							mods = append(mods, s)
						}
					}
				}
				res = append(res, binding{Path: append([]int(nil), path...), Event: event, Handler: v, Modifiers: mods})
			}
		}
	}
	child := n.FirstChild
	idx := 0
	for child != nil {
		res = append(res, collectBindings(child, append(path, idx))...)
		child = child.NextSibling
		idx++
	}
	return res
}
F
function

replaceEventHandlers

Parameters

template
string

Returns

string
dom/bindings.go:99-120
func replaceEventHandlers(template string) string

{
	return eventRegex.ReplaceAllStringFunc(template, func(match string) string {
		parts := eventRegex.FindStringSubmatch(match)
		if len(parts) != 5 {
			return match
		}
		fullEvent := parts[2]
		handler := parts[3]
		suffix := parts[4]
		eventParts := strings.Split(fullEvent, ".")
		event := eventParts[0]
		modifiers := []string{}
		if len(eventParts) > 1 {
			modifiers = eventParts[1:]
		}
		attr := fmt.Sprintf("data-on-%s=\"%s\"", event, handler)
		if len(modifiers) > 0 {
			attr += fmt.Sprintf(" data-on-%s-modifiers=\"%s\"", event, strings.Join(modifiers, ","))
		}
		return attr + suffix
	})
}
F
function

TestDocumentElementBasics

Parameters

dom/document_test.go:7-14
func TestDocumentElementBasics(t *testing.T)

{
	doc := Doc()
	el := doc.CreateElement("div")
	el.SetText("hello")
	if got := el.Text(); got != "hello" {
		t.Fatalf("Text() = %q", got)
	}
}
F
function

TestDocumentHead

Parameters

dom/document_test.go:16-21
func TestDocumentHead(t *testing.T)

{
	doc := Doc()
	if node := doc.Head().Get("nodeName").String(); node != "HEAD" {
		t.Fatalf("Head() node = %q", node)
	}
}
F
function

TestPlaceholder

Parameters

dom/dom_test.go:5-5
func TestPlaceholder(_ *testing.T)

{}
F
function

TestElementRemoveAttr

Parameters

dom/element_helpers_test.go:7-17
func TestElementRemoveAttr(t *testing.T)

{
	el := Doc().CreateElement("button")
	el.SetAttr("disabled", "")
	if !el.Call("hasAttribute", "disabled").Bool() {
		t.Fatal("SetAttr did not set disabled")
	}
	el.RemoveAttr("disabled")
	if el.Call("hasAttribute", "disabled").Bool() {
		t.Fatal("RemoveAttr left the attribute in place")
	}
}
F
function

TestElementMatches

Parameters

dom/element_helpers_test.go:19-28
func TestElementMatches(t *testing.T)

{
	el := Doc().CreateElement("div")
	el.SetAttr("data-row", "1")
	if !el.Matches("[data-row]") {
		t.Fatal("Matches() = false for a matching selector")
	}
	if el.Matches("[data-other]") {
		t.Fatal("Matches() = true for a non-matching selector")
	}
}
F
function

TestMissingElementHelpersAreSafe

Parameters

dom/element_helpers_test.go:30-36
func TestMissingElementHelpersAreSafe(t *testing.T)

{
	el := Query("#definitely-not-in-the-document")
	el.RemoveAttr("disabled")
	if el.Matches("[data-row]") {
		t.Fatal("Matches() = true on a missing element")
	}
}
F
function

TestFromWrapsRawValue

Parameters

dom/element_helpers_test.go:38-53
func TestFromWrapsRawValue(t *testing.T)

{
	parent := Doc().CreateElement("div")
	parent.SetHTML(`<span data-id="7" class="a">x</span>`)
	raw := parent.Call("querySelector", "span")

	el := From(raw)
	if got := el.Data("id"); got != "7" {
		t.Fatalf("Data(id) = %q", got)
	}
	if !el.HasClass("a") {
		t.Fatal("HasClass(a) = false")
	}
	if c := el.Closest("div"); c.IsNull() {
		t.Fatal("Closest(div) returned null")
	}
}
F
function

CreateElement

CreateElement returns a new element with the given tag name.

Parameters

tag
string

Returns

dom/selectors.go:8-8
func CreateElement(tag string) Element

{ return Doc().CreateElement(tag) }
F
function

From

From wraps a raw value into the typed element API. Elements that reach
application code from outside the query helpers (an event target, a NodeList
entry, a node returned by a browser API) have no other way in, and without
it callers fall back to getAttribute/classList/closest by hand.

Parameters

Returns

dom/selectors.go:14-14
func From(v js.Value) Element

{ return Element{v} }
F
function

ByID

ByID fetches an element by its id attribute.

Parameters

id
string

Returns

dom/selectors.go:17-17
func ByID(id string) Element

{ return Doc().ByID(id) }
F
function

Query

Query returns the first element matching the CSS selector.

Parameters

selector
string

Returns

dom/selectors.go:20-20
func Query(selector string) Element

{ return Doc().Query(selector) }
F
function

QueryAll

QueryAll returns all elements matching the CSS selector.

Parameters

selector
string

Returns

dom/selectors.go:23-23
func QueryAll(selector string) Element

{ return Doc().QueryAll(selector) }
F
function

ByClass

ByClass returns all elements with the given class name.

Parameters

name
string

Returns

dom/selectors.go:26-26
func ByClass(name string) Element

{ return Doc().ByClass(name) }
F
function

ByTag

ByTag returns all elements with the given tag name.

Parameters

tag
string

Returns

dom/selectors.go:29-29
func ByTag(tag string) Element

{ return Doc().ByTag(tag) }
F
function

SetInnerHTML

SetInnerHTML replaces an element’s children with the provided HTML string.

Parameters

el
html
string
dom/selectors.go:32-32
func SetInnerHTML(el Element, html string)

{ el.SetHTML(html) }
F
function

Text

Text returns an element’s text content.

Parameters

el

Returns

string
dom/selectors.go:35-35
func Text(el Element) string

{ return el.Text() }
F
function

SetText

SetText sets an element’s text content.

Parameters

el
text
string
dom/selectors.go:38-38
func SetText(el Element, text string)

{ el.SetText(text) }
F
function

Attr

Attr retrieves the value of an attribute or an empty string if unset.

Parameters

el
name
string

Returns

string
dom/selectors.go:41-41
func Attr(el Element, name string) string

{ return el.Attr(name) }
F
function

SetAttr

SetAttr sets the value of an attribute on the element.

Parameters

el
name
string
value
string
dom/selectors.go:44-44
func SetAttr(el Element, name, value string)

{ el.SetAttr(name, value) }
F
function

AddClass

AddClass adds a class to the element’s class list.

Parameters

el
class
string
dom/selectors.go:47-47
func AddClass(el Element, class string)

{ el.AddClass(class) }
F
function

RemoveClass

RemoveClass removes a class from the element’s class list.

Parameters

el
class
string
dom/selectors.go:50-50
func RemoveClass(el Element, class string)

{ el.RemoveClass(class) }
F
function

HasClass

HasClass reports whether the element has the specified class.

Parameters

el
class
string

Returns

bool
dom/selectors.go:53-53
func HasClass(el Element, class string) bool

{ return el.HasClass(class) }
F
function

ToggleClass

ToggleClass toggles the presence of a class on the element’s class list.

Parameters

el
class
string
dom/selectors.go:56-56
func ToggleClass(el Element, class string)

{ el.ToggleClass(class) }
F
function

SetStyle

SetStyle sets an inline style property on the element.

Parameters

el
prop
string
value
string
dom/selectors.go:59-59
func SetStyle(el Element, prop, value string)

{ el.SetStyle(prop, value) }
F
function

eventOptions

Returns

dom/handlers_test.go:12-17
func eventOptions() js.Dict

{
	opts := js.NewDict()
	opts.Set("bubbles", true)
	opts.Set("cancelable", true)
	return opts
}
F
function

mountHandlerRoot

Parameters

html
string

Returns

dom/handlers_test.go:19-26
func mountHandlerRoot(t *testing.T, html string) Element

{
	t.Helper()
	root := Doc().CreateElement("div")
	root.SetHTML(html)
	Doc().Body().AppendChild(root)
	t.Cleanup(func() { root.Call("remove") })
	return root
}
F
function

TestComponentHandlersAreScoped

Parameters

dom/handlers_test.go:28-49
func TestComponentHandlersAreScoped(t *testing.T)

{
	firstRoot := mountHandlerRoot(t, `<button data-on-click="save">one</button>`)
	secondRoot := mountHandlerRoot(t, `<button data-on-click="save">two</button>`)
	var first, second int

	RegisterComponentHandlerFunc("first", "save", func() { first++ })
	RegisterComponentHandlerFunc("second", "save", func() { second++ })
	DelegateEvents("first", firstRoot.Value)
	DelegateEvents("second", secondRoot.Value)
	t.Cleanup(func() {
		RemoveDelegatedEvents("first", firstRoot.Value)
		RemoveDelegatedEvents("second", secondRoot.Value)
		ReleaseComponentHandlers("first")
		ReleaseComponentHandlers("second")
	})

	firstRoot.Query("button").Call("click")
	secondRoot.Query("button").Call("click")
	if first != 1 || second != 1 {
		t.Fatalf("scoped handler counts = %d, %d", first, second)
	}
}
F
function

TestDelegatedEventModifiers

Parameters

dom/handlers_test.go:51-70
func TestDelegatedEventModifiers(t *testing.T)

{
	root := mountHandlerRoot(t, `<button data-on-click="save" data-on-click-modifiers="prevent,once">save</button>`)
	var calls int
	RegisterComponentHandlerFunc("modifiers", "save", func() { calls++ })
	DelegateEvents("modifiers", root.Value)
	t.Cleanup(func() {
		RemoveDelegatedEvents("modifiers", root.Value)
		ReleaseComponentHandlers("modifiers")
	})

	button := root.Query("button")
	first := js.Get("MouseEvent").New("click", eventOptions().Value)
	if allowed := button.Call("dispatchEvent", first).Bool(); allowed {
		t.Fatal("prevent modifier did not cancel the event")
	}
	button.Call("dispatchEvent", js.Get("MouseEvent").New("click", eventOptions().Value))
	if calls != 1 {
		t.Fatalf("once handler calls = %d", calls)
	}
}
F
function

TestDelegatedKeyAndTimingModifiers

Parameters

dom/handlers_test.go:72-114
func TestDelegatedKeyAndTimingModifiers(t *testing.T)

{
	root := mountHandlerRoot(t, `
		<input data-on-keydown="submit" data-on-keydown-modifiers="enter">
		<button id="debounce" data-on-click="search" data-on-click-modifiers="debounce,10">search</button>
		<button id="throttle" data-on-click="refresh" data-on-click-modifiers="throttle,10">refresh</button>
	`)
	var submit, search, refresh int
	RegisterComponentHandlerFunc("timing", "submit", func() { submit++ })
	RegisterComponentHandlerFunc("timing", "search", func() { search++ })
	RegisterComponentHandlerFunc("timing", "refresh", func() { refresh++ })
	DelegateEvents("timing", root.Value)
	t.Cleanup(func() {
		RemoveDelegatedEvents("timing", root.Value)
		ReleaseComponentHandlers("timing")
	})

	input := root.Query("input")
	escape := eventOptions()
	escape.Set("key", "Escape")
	input.Call("dispatchEvent", js.Get("KeyboardEvent").New("keydown", escape.Value))
	enter := eventOptions()
	enter.Set("key", "Enter")
	input.Call("dispatchEvent", js.Get("KeyboardEvent").New("keydown", enter.Value))
	if submit != 1 {
		t.Fatalf("enter handler calls = %d", submit)
	}

	debounce := root.Query("#debounce")
	debounce.Call("click")
	debounce.Call("click")
	throttle := root.Query("#throttle")
	throttle.Call("click")
	throttle.Call("click")
	time.Sleep(30 * time.Millisecond)
	throttle.Call("click")

	if search != 1 {
		t.Fatalf("debounce handler calls = %d", search)
	}
	if refresh != 2 {
		t.Fatalf("throttle handler calls = %d", refresh)
	}
}
F
function

TestDelegatedFocusHandlerUsesCaptureListener

Parameters

dom/handlers_test.go:116-132
func TestDelegatedFocusHandlerUsesCaptureListener(t *testing.T)

{
	root := mountHandlerRoot(t, `<input data-on-focus="focus">`)
	var calls int
	RegisterComponentHandlerFunc("focus", "focus", func() { calls++ })
	DelegateEvents("focus", root.Value)
	t.Cleanup(func() {
		RemoveDelegatedEvents("focus", root.Value)
		ReleaseComponentHandlers("focus")
	})

	options := js.NewDict()
	options.Set("bubbles", false)
	root.Query("input").Call("dispatchEvent", js.Get("FocusEvent").New("focus", options.Value))
	if calls != 1 {
		t.Fatalf("focus handler calls = %d", calls)
	}
}
S
struct

LifecycleHook

LifecycleHook observes a component root after mount, update, and unmount.

dom/lifecycle.go:11-15
type LifecycleHook struct

Fields

Name Type Description
Mounted func(Element) func()
Updated func(Element)
Unmounted func(Element)
S
struct

lifecycleRecord

dom/lifecycle.go:17-23
type lifecycleRecord struct

Methods

setCleanup
Method

Parameters

cleanup func()
func (*lifecycleRecord) setCleanup(cleanup func())
{
	record.mu.Lock()
	if record.stopped {
		record.mu.Unlock()
		if cleanup != nil {
			cleanup()
		}
		return
	}
	record.cleanup = cleanup
	record.mu.Unlock()
}
takeCleanup
Method

Parameters

stop bool

Returns

func()
func (*lifecycleRecord) takeCleanup(stop bool) func()
{
	record.mu.Lock()
	cleanup := record.cleanup
	record.cleanup = nil
	if stop {
		record.stopped = true
	}
	record.mu.Unlock()
	return cleanup
}

Fields

Name Type Description
id uint64
hook LifecycleHook
mu sync.Mutex
cleanup func()
stopped bool
S
struct

componentLifecycle

dom/lifecycle.go:49-52
type componentLifecycle struct

Fields

Name Type Description
mounted bool
hooks []*lifecycleRecord
F
function

RegisterLifecycleHook

RegisterLifecycleHook registers a hook and returns a cancellation function.

Parameters

componentID
string

Returns

func()
dom/lifecycle.go:61-99
func RegisterLifecycleHook(componentID string, hook LifecycleHook) func()

{
	record := &lifecycleRecord{id: lifecycleHooks.sequence.Add(1), hook: hook}
	lifecycleHooks.Lock()
	component := lifecycleHooks.components[componentID]
	if component == nil {
		component = &componentLifecycle{}
		lifecycleHooks.components[componentID] = component
	}
	component.hooks = append(component.hooks, record)
	mounted := component.mounted
	lifecycleHooks.Unlock()

	if mounted && hook.Mounted != nil {
		record.setCleanup(runMountedHook(componentID, hook.Mounted, ownComponentRoot(componentID)))
	}
	var once sync.Once
	return func() {
		once.Do(func() {
			lifecycleHooks.Lock()
			component := lifecycleHooks.components[componentID]
			if component != nil {
				for index, candidate := range component.hooks {
					if candidate.id == record.id {
						component.hooks = append(component.hooks[:index], component.hooks[index+1:]...)
						break
					}
				}
				if len(component.hooks) == 0 {
					delete(lifecycleHooks.components, componentID)
				}
			}
			cleanup := record.takeCleanup(true)
			lifecycleHooks.Unlock()
			if cleanup != nil {
				cleanup()
			}
		})
	}
}
F
function

MountLifecycleHooks

MountLifecycleHooks activates every hook registered for a component.

Parameters

componentID
string
dom/lifecycle.go:102-118
func MountLifecycleHooks(componentID string)

{
	lifecycleHooks.Lock()
	component := lifecycleHooks.components[componentID]
	if component == nil || component.mounted {
		lifecycleHooks.Unlock()
		return
	}
	component.mounted = true
	hooks := append([]*lifecycleRecord(nil), component.hooks...)
	lifecycleHooks.Unlock()
	root := ownComponentRoot(componentID)
	for _, record := range hooks {
		if record.hook.Mounted != nil {
			record.setCleanup(runMountedHook(componentID, record.hook.Mounted, root))
		}
	}
}
F
function

UpdateLifecycleHooks

UpdateLifecycleHooks notifies mounted component hooks after a DOM patch.

Parameters

componentID
string
dom/lifecycle.go:121-138
func UpdateLifecycleHooks(componentID string)

{
	lifecycleHooks.Lock()
	component := lifecycleHooks.components[componentID]
	if component == nil || !component.mounted {
		lifecycleHooks.Unlock()
		return
	}
	hooks := append([]*lifecycleRecord(nil), component.hooks...)
	lifecycleHooks.Unlock()
	root := ownComponentRoot(componentID)
	for _, record := range hooks {
		if record.hook.Updated != nil {
			runLifecycleHook(componentID, "updated", func() {
				record.hook.Updated(root)
			})
		}
	}
}
F
function

UnmountLifecycleHooks

UnmountLifecycleHooks runs hook cleanup while the component root still exists.

Parameters

componentID
string
dom/lifecycle.go:141-163
func UnmountLifecycleHooks(componentID string)

{
	lifecycleHooks.Lock()
	component := lifecycleHooks.components[componentID]
	if component == nil || !component.mounted {
		lifecycleHooks.Unlock()
		return
	}
	component.mounted = false
	hooks := append([]*lifecycleRecord(nil), component.hooks...)
	lifecycleHooks.Unlock()
	root := ownComponentRoot(componentID)
	for index := len(hooks) - 1; index >= 0; index-- {
		record := hooks[index]
		if cleanup := record.takeCleanup(false); cleanup != nil {
			runLifecycleHook(componentID, "cleanup", cleanup)
		}
		if record.hook.Unmounted != nil {
			runLifecycleHook(componentID, "unmounted", func() {
				record.hook.Unmounted(root)
			})
		}
	}
}
F
function

runMountedHook

Parameters

componentID
string
hook
func(Element) func()
root

Returns

cleanup
func()
dom/lifecycle.go:165-170
func runMountedHook(componentID string, hook func(Element) func(), root Element) (cleanup func())

{
	runLifecycleHook(componentID, "mounted", func() {
		cleanup = hook(root)
	})
	return cleanup
}
F
function

runLifecycleHook

Parameters

componentID
string
phase
string
fn
func()
dom/lifecycle.go:172-179
func runLifecycleHook(componentID, phase string, fn func())

{
	defer func() {
		if recovered := recover(); recovered != nil && OnHandlerPanic != nil {
			OnHandlerPanic(recovered, "DOM "+phase+": "+componentID)
		}
	}()
	fn()
}
F
function

ownComponentRoot

Parameters

componentID
string

Returns

dom/lifecycle.go:181-187
func ownComponentRoot(componentID string) Element

{
	root := ComponentRoot(componentID)
	if root.IsNull() || root.IsUndefined() || root.Attr("data-component-id") != componentID {
		return Element{}
	}
	return root
}
F
function

TestExpandEvents

Parameters

dom/markup_test.go:5-18
func TestExpandEvents(t *testing.T)

{
	cases := []struct{ in, want string }{
		{`<button @on:click:save>`, `<button data-on-click="save">`},
		{`<button @click:save>`, `<button data-on-click="save">`},
		{`<input @on:keydown.enter:submit />`, `<input data-on-keydown="submit" data-on-keydown-modifiers="enter" />`},
		{`<tr @on:click:openRow data-id="3">`, `<tr data-on-click="openRow" data-id="3">`},
		{`plain text with an [email protected] stays`, `plain text with an [email protected] stays`},
	}
	for _, c := range cases {
		if got := ExpandEvents(c.in); got != c.want {
			t.Errorf("ExpandEvents(%q) = %q, want %q", c.in, got, c.want)
		}
	}
}