dom API

dom

package

API reference for the dom package.

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()
	})
}
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

addInputBindingStop

Parameters

componentID
string
stop
func()
dom/dom.go:34-38
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:42-50
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:58-65
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:68-72
func RemoveComponentSignals(componentID string)

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

getSignal

Parameters

componentID
string
name
string

Returns

any
dom/dom.go:74-81
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:84-95
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
}
S
struct

recoveredDOMPanic

dom/dom.go:106-109
type recoveredDOMPanic struct

Methods

Error
Method

Returns

string
func (recoveredDOMPanic) Error() string
{ return fmt.Sprint(p.value) }
Stack
Method

Returns

[]byte
func (recoveredDOMPanic) Stack() []byte
{ return p.stack }

Fields

Name Type Description
value any
stack []byte
F
function

recoverDOMUpdate

Parameters

componentID
string
dom/dom.go:114-131
func recoverDOMUpdate(componentID string)

{
	if recovered := recover(); recovered != nil {
		panicValue := recoveredDOMPanic{value: recovered, stack: debug.Stack()}
		if OnHandlerPanic == nil {
			log.Printf("[rfw] recovered DOM update panic for %s: %v\n%s", componentID, recovered, panicValue.stack)
			return
		}
		func() {
			defer func() {
				if hookPanic := recover(); hookPanic != nil {
					log.Printf("[rfw] DOM panic reporter failed: %v", hookPanic)
					log.Printf("[rfw] recovered DOM update panic for %s: %v\n%s", componentID, recovered, panicValue.stack)
				}
			}()
			OnHandlerPanic(panicValue, "DOM update: "+componentID)
		}()
	}
}
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:135-145
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:149-185
func UpdateDOM(componentID string, html string)

{
	defer recoverDOMUpdate(componentID)
	element := ComponentRoot(componentID)
	if element.IsNull() || element.IsUndefined() {
		return
	}
	activeForm := captureActiveFormState(element.Value)

	// 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)
		recordRenderedTree(element.Value)
	}

	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)
	activeForm.restore()
	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:191-201
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:206-225
func UpdateDOMIn(target Element, componentID, html string)

{
	defer recoverDOMUpdate(componentID)
	if target.IsNull() || target.IsUndefined() {
		return
	}
	target.Set("innerHTML", html)
	recordRenderedTree(target.Value)

	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:229-282
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)
		if !componentOwnsElement(componentID, input) {
			continue
		}
		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 {
					setCheckedIfChanged(input, 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 = ""
		}
		setValueIfChanged(input, fmt.Sprintf("%v", storeValue))
		ch, stop := events.Listen("input", input)
		addInputBindingStop(componentID, stop)
		go func(in js.Value, st *state.Store, k string) {
			for event := range ch {
				if inputEventIsComposing(event) {
					continue
				}
				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:286-337
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)
		if !componentOwnsElement(componentID, input) {
			continue
		}
		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 {
						setCheckedIfChanged(input, 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 {
			setValueIfChanged(input, fmt.Sprintf("%v", s.Read()))
			ch, stop := events.Listen("input", input)
			addInputBindingStop(componentID, stop)
			go func(in js.Value, sg interface{ Set(string) }) {
				for event := range ch {
					if inputEventIsComposing(event) {
						continue
					}
					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:341-412
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)
		if !componentOwnsElement(componentID, input) {
			continue
		}

		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)
			setCheckedIfChanged(input, 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 = ""
		}
		setValueIfChanged(input, fmt.Sprintf("%v", storeValue))
		ch, stop := events.Listen("input", input)
		addInputBindingStop(componentID, stop)
		go func(in js.Value, st *state.Store, k string) {
			for event := range ch {
				if inputEventIsComposing(event) {
					continue
				}
				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:415-417
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:420-493
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)
		if !componentOwnsElement(componentID, input) {
			continue
		}

		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 {
					setCheckedIfChanged(input, 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 {
			setValueIfChanged(input, fmt.Sprintf("%v", s.Read()))
			ch, stop := events.Listen("input", input)
			addInputBindingStop(componentID, stop)
			go func(in js.Value, sg interface{ Set(string) }) {
				for event := range ch {
					if inputEventIsComposing(event) {
						continue
					}
					sg.Set(in.Get("value").String())
				}
			}(input, s)
		}
	}
}
F
function

componentOwnsElement

Parameters

componentID
string
element

Returns

bool
dom/dom.go:495-501
func componentOwnsElement(componentID string, element js.Value) bool

{
	if componentID == "" {
		return true
	}
	root := element.Call("closest", "[data-component-id]")
	return root.Truthy() && attribute(root, "data-component-id") == componentID
}
F
function

setValueIfChanged

Parameters

element
value
string
dom/dom.go:503-507
func setValueIfChanged(element js.Value, value string)

{
	if element.Get("value").String() != value {
		element.Set("value", value)
	}
}
F
function

setCheckedIfChanged

Parameters

element
checked
bool
dom/dom.go:509-513
func setCheckedIfChanged(element js.Value, checked bool)

{
	if element.Get("checked").Bool() != checked {
		element.Set("checked", checked)
	}
}
F
function

inputEventIsComposing

Parameters

event

Returns

bool
dom/dom.go:515-518
func inputEventIsComposing(event js.Value) bool

{
	value := event.Get("isComposing")
	return value.Type() == js.TypeBoolean && value.Bool()
}
F
function

TestUpdateDOMSkipsNonElementNodes

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

Parameters

dom/dom_patch_test.go:15-24
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-->")
}
F
function

TestPatchFocusedNumberInputPreservesLiveValue

Parameters

dom/dom_patch_test.go:26-53
func TestPatchFocusedNumberInputPreservesLiveValue(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "number-input")
	root.SetHTML(`<input id="stake" type="number" value="25"><span>old</span>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	input := root.Query("#stake")
	input.SetValue("28")
	input.Call("focus")

	patchInnerHTML(root.Value, `<root data-component-id="number-input"><input id="stake" type="number" value="25"><span>new</span></root>`)

	patched := root.Query("#stake")
	if !patched.Equal(input.Value) {
		t.Fatal("number input was replaced")
	}
	if got := patched.Val(); got != "28" {
		t.Fatalf("number input value = %q, want 28", got)
	}
	if !js.Doc().Get("activeElement").Equal(patched.Value) {
		t.Fatal("number input lost focus")
	}
	if got := root.Query("span").Text(); got != "new" {
		t.Fatalf("patched text = %q, want new", got)
	}
}
F
function

TestPatchPreservesFocusedTextInputIdentityAndCaret

Parameters

dom/dom_patch_test.go:55-86
func TestPatchPreservesFocusedTextInputIdentityAndCaret(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "search-input")
	root.SetHTML(`<input type="search" value="china"><span>old</span>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	input := root.Query("input")
	input.SetValue("chinaa")
	input.Call("focus")
	input.Call("setSelectionRange", 2, 4, "forward")

	patchInnerHTML(root.Value, `<root data-component-id="search-input"><input type="search" value="server"><span>new</span></root>`)

	patched := root.Query("input")
	if !patched.Equal(input.Value) {
		t.Fatal("search input was replaced")
	}
	if !js.Doc().Get("activeElement").Equal(patched.Value) {
		t.Fatal("search input lost focus")
	}
	if got := patched.Val(); got != "chinaa" {
		t.Fatalf("live input value = %q, want chinaa", got)
	}
	if got := patched.Get("selectionStart").Int(); got != 2 {
		t.Fatalf("selection start = %d, want 2", got)
	}
	if got := patched.Get("selectionEnd").Int(); got != 4 {
		t.Fatalf("selection end = %d, want 4", got)
	}
}
F
function

TestPatchDoesNotRestoreDeliberatelyBlurredInput

Parameters

dom/dom_patch_test.go:88-105
func TestPatchDoesNotRestoreDeliberatelyBlurredInput(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "blurred-input")
	root.SetHTML(`<input type="search"><button>Apply</button>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	input := root.Query("input")
	input.Call("focus")
	input.Call("blur")

	patchInnerHTML(root.Value, `<root data-component-id="blurred-input"><input type="search"><button>Updated</button></root>`)

	if js.Doc().Get("activeElement").Equal(input.Value) {
		t.Fatal("patch restored focus after an explicit blur")
	}
}
F
function

TestPatchPreservesUncontrolledFormProperties

Parameters

dom/dom_patch_test.go:107-138
func TestPatchPreservesUncontrolledFormProperties(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "form-state")
	root.SetHTML(`<textarea>initial</textarea><input type="checkbox"><input type="radio" name="choice"><select><option>A</option><option>B</option></select>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	textarea := root.Query("textarea")
	checkbox := root.Query(`input[type="checkbox"]`)
	radio := root.Query(`input[type="radio"]`)
	selectEl := root.Query("select")
	textarea.SetValue("operator text")
	checkbox.Set("checked", true)
	radio.Set("checked", true)
	selectEl.Set("selectedIndex", 1)

	patchInnerHTML(root.Value, `<root data-component-id="form-state"><textarea>server text</textarea><input type="checkbox"><input type="radio" name="choice"><select><option selected>A</option><option>B</option></select></root>`)

	if !root.Query("textarea").Equal(textarea.Value) || root.Query("textarea").Val() != "operator text" {
		t.Fatal("textarea identity or live value was not preserved")
	}
	if !root.Query(`input[type="checkbox"]`).Equal(checkbox.Value) || !checkbox.Checked() {
		t.Fatal("checkbox identity or checked state was not preserved")
	}
	if !root.Query(`input[type="radio"]`).Equal(radio.Value) || !radio.Checked() {
		t.Fatal("radio identity or checked state was not preserved")
	}
	if !root.Query("select").Equal(selectEl.Value) || selectEl.Get("selectedIndex").Int() != 1 {
		t.Fatal("select identity or selected option was not preserved")
	}
}
F
function

TestPatchPreservesAndReordersKeyedNodes

Parameters

dom/dom_patch_test.go:140-159
func TestPatchPreservesAndReordersKeyedNodes(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "keyed-list")
	root.SetHTML(`<ul><li data-key="a">A</li><li data-key="b">B</li></ul>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	a := root.Query(`[data-key="a"]`)
	b := root.Query(`[data-key="b"]`)
	patchInnerHTML(root.Value, `<root data-component-id="keyed-list"><ul><li data-key="b">B2</li><li data-key="a">A2</li><li data-key="c">C</li></ul></root>`)

	rows := root.QueryAll("li")
	if rows.Length() != 3 || !rows.Index(0).Equal(b.Value) || !rows.Index(1).Equal(a.Value) {
		t.Fatalf("keyed rows lost identity or order: %s", root.HTML())
	}
	if rows.Index(0).Text() != "B2" || rows.Index(1).Text() != "A2" {
		t.Fatalf("keyed row contents were not patched: %s", root.HTML())
	}
}
F
function

TestInvalidPatchPlanLeavesDOMUntouched

Parameters

dom/dom_patch_test.go:161-186
func TestInvalidPatchPlanLeavesDOMUntouched(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "atomic-plan")
	root.SetHTML(`<ul><li data-key="a">A</li><li data-key="b">B</li></ul>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	before := root.HTML()
	a := root.Query(`[data-key="a"]`)
	var recovered any
	func() {
		defer func() { recovered = recover() }()
		patchInnerHTML(root.Value, `<root data-component-id="atomic-plan"><ul><li data-key="a">changed</li><li data-key="a">duplicate</li></ul></root>`)
	}()

	if recovered == nil {
		t.Fatal("duplicate identity did not reject the patch")
	}
	if got := root.HTML(); got != before {
		t.Fatalf("invalid plan mutated DOM: got %s, want %s", got, before)
	}
	if !root.Query(`[data-key="a"]`).Equal(a.Value) {
		t.Fatal("invalid plan replaced a node before failing")
	}
}
F
function

TestKeyIdentityIsScopedToItsLoop

Parameters

dom/dom_patch_test.go:188-201
func TestKeyIdentityIsScopedToItsLoop(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "loop-scopes")
	root.SetHTML(`<section><i data-for="first" data-key="0">A</i><i data-for="second" data-key="0">B</i></section>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	patchInnerHTML(root.Value, `<root data-component-id="loop-scopes"><section><i data-for="first" data-key="0">A2</i><i data-for="second" data-key="0">B2</i></section></root>`)
	rows := root.QueryAll("i")
	if rows.Length() != 2 || rows.Index(0).Text() != "A2" || rows.Index(1).Text() != "B2" {
		t.Fatalf("loop-scoped keys were treated as duplicates: %s", root.HTML())
	}
}
F
function

TestPatchRespectsNestedDOMOwnership

Parameters

dom/dom_patch_test.go:203-227
func TestPatchRespectsNestedDOMOwnership(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "shell")
	root.SetHTML(`<span data-shell>old</span><root data-component-id="child"><div data-child>rendered</div></root><div data-router-outlet><root data-component-id="page"><div data-page>mounted</div></root></div>`)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	child := root.Query(`[data-component-id="child"]`)
	page := root.Query(`[data-component-id="page"]`)
	child.Query("[data-child]").SetHTML("imperative child")
	page.Query("[data-page]").SetHTML("imperative page")

	patchInnerHTML(root.Value, `<root data-component-id="shell"><span data-shell>new</span><root data-component-id="child"><div data-child>stale render</div></root><div data-router-outlet><p>empty render</p></div></root>`)

	if root.Query("[data-shell]").Text() != "new" {
		t.Fatal("shell-owned node was not patched")
	}
	if !root.Query(`[data-component-id="child"]`).Equal(child.Value) || child.Query("[data-child]").Text() != "imperative child" {
		t.Fatal("parent patch crossed child component ownership")
	}
	if !root.Query(`[data-component-id="page"]`).Equal(page.Value) || page.Query("[data-page]").Text() != "imperative page" {
		t.Fatal("parent patch crossed router outlet ownership")
	}
}
F
function

TestPatchPreservesLiveAttributesUntilTemplateChangesThem

Parameters

dom/dom_patch_test.go:229-253
func TestPatchPreservesLiveAttributesUntilTemplateChangesThem(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "live-attributes")
	root.SetHTML(`<section class="panel" aria-expanded="false"><span>old</span></section>`)
	recordRenderedTree(root.Value)
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	panel := root.Query("section")
	panel.AddClass("open")
	panel.SetAttr("aria-expanded", "true")
	patchInnerHTML(root.Value, `<root data-component-id="live-attributes"><section class="panel" aria-expanded="false"><span>new</span></section></root>`)
	if !panel.HasClass("open") || panel.Attr("aria-expanded") != "true" {
		t.Fatalf("unchanged template attributes erased live state: %s", root.HTML())
	}
	if panel.Query("span").Text() != "new" {
		t.Fatal("attribute preservation blocked descendant patching")
	}

	patchInnerHTML(root.Value, `<root data-component-id="live-attributes"><section class="panel disabled" aria-expanded="mixed"><span>latest</span></section></root>`)
	if panel.Attr("class") != "panel disabled" || panel.Attr("aria-expanded") != "mixed" {
		t.Fatalf("changed template attributes did not take ownership: %s", root.HTML())
	}
}
F
function

TestUpdateDOMRecoversAndAcceptsNextUpdate

Parameters

dom/dom_patch_test.go:255-283
func TestUpdateDOMRecoversAndAcceptsNextUpdate(t *testing.T)

{
	body := js.Doc().Get("body")
	root := CreateElement("root")
	root.SetAttr("data-component-id", "recover-update")
	root.SetHTML("<span>old</span>")
	body.Call("appendChild", root.Value)
	defer root.Call("remove")

	previousHook := TemplateHook
	previousPanic := OnHandlerPanic
	defer func() {
		TemplateHook = previousHook
		OnHandlerPanic = previousPanic
	}()
	recovered := 0
	OnHandlerPanic = func(any, string) { recovered++ }
	TemplateHook = func(string, string) { panic("template hook") }

	UpdateDOM("recover-update", `<root data-component-id="recover-update"><span>first</span></root>`)
	TemplateHook = nil
	UpdateDOM("recover-update", `<root data-component-id="recover-update"><span>second</span></root>`)

	if recovered != 1 {
		t.Fatalf("recovered updates = %d, want 1", recovered)
	}
	if got := root.Query("span").Text(); got != "second" {
		t.Fatalf("next update text = %q, want second", got)
	}
}
F
function

TestDOMReporterPanicLogsOriginalFailure

Parameters

dom/dom_patch_test.go:285-308
func TestDOMReporterPanicLogsOriginalFailure(t *testing.T)

{
	previousPanic := OnHandlerPanic
	previousWriter := log.Writer()
	var output bytes.Buffer
	OnHandlerPanic = func(any, string) { panic("reporter failure") }
	log.SetOutput(&output)
	defer func() {
		OnHandlerPanic = previousPanic
		log.SetOutput(previousWriter)
	}()

	func() {
		defer recoverDOMUpdate("broken-component")
		panic("original failure")
	}()

	logs := output.String()
	if !strings.Contains(logs, "DOM panic reporter failed: reporter failure") {
		t.Fatalf("missing reporter failure log: %s", logs)
	}
	if !strings.Contains(logs, "recovered DOM update panic for broken-component: original failure") {
		t.Fatalf("missing original failure log: %s", logs)
	}
}
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-266
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() {
						key := eventBindingKey(target, event, handlerName.String())
						if !claimEventBinding(evt, key) {
							if target.Equal(root) {
								break
							}
							target = target.Get("parentElement")
							continue
						}
						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)
						}
						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

claimEventBinding

Parameters

evt
key
string

Returns

bool
dom/handlers.go:268-280
func claimEventBinding(evt js.Value, key string) bool

{
	const property = "__rfwDelegatedClaims"
	claims := evt.Get(property)
	if claims.Type() != js.TypeObject {
		claims = js.NewDict().Value
		evt.Set(property, claims)
	}
	if claims.Get(key).Truthy() {
		return false
	}
	claims.Set(key, true)
	return true
}
F
function

eventModifiers

Parameters

target
event
string

Returns

map[string]struct{}
dom/handlers.go:282-295
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:297-325
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:327-339
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:341-350
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:353-373
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

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
}
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

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

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

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
	})
}
S
struct

patchPlan

patchPlan separates reconciliation decisions from DOM mutation. Planning is
read-only: invalid identities or ownership conflicts are reported before the
first operation is committed.

dom/reconcile.go:17-20
type patchPlan struct

Methods

commit
Method
func (*patchPlan) commit()
{
	for _, operation := range plan.ops {
		operation()
	}
}
planNode
Method

Parameters

existing js.Value
replacement js.Value

Returns

error
func (*patchPlan) planNode(existing, replacement js.Value) error
{
	if existing.Get("nodeType").Int() != replacement.Get("nodeType").Int() ||
		existing.Get("nodeName").String() != replacement.Get("nodeName").String() {
		return fmt.Errorf("rfw DOM patch: incompatible nodes %s and %s", existing.Get("nodeName").String(), replacement.Get("nodeName").String())
	}

	nodeType := replacement.Get("nodeType").Int()
	if nodeType == 3 || nodeType == 8 { // text or comment
		oldValue := existing.Get("nodeValue").String()
		newValue := replacement.Get("nodeValue").String()
		if oldValue != newValue {
			plan.ops = append(plan.ops, func() { existing.Set("nodeValue", newValue) })
		}
		return nil
	}
	if nodeType != 1 {
		return plan.planChildren(existing, replacement)
	}

	// A parent owns the presence of a child component, never its internals. The
	// child schedules and patches its own root. Router outlets follow the same
	// rule for their contents while allowing the shell to own outlet attributes.
	componentID := attribute(existing, "data-component-id")
	if componentID != "" && componentID != plan.ownerID {
		if componentID != attribute(replacement, "data-component-id") {
			return fmt.Errorf("rfw DOM patch: component ownership changed from %q", componentID)
		}
		return nil
	}

	formState := captureLiveFormState(existing, replacement)
	plan.planAttributes(existing, replacement)
	if isRouterOutlet(existing) && isRouterOutlet(replacement) {
		return nil
	}
	if attribute(existing, "data-condition") != "" &&
		attribute(existing, "data-condition-branch") != attribute(replacement, "data-condition-branch") {
		return plan.planReplaceChildren(existing, replacement)
	}
	if err := plan.planChildren(existing, replacement); err != nil {
		return err
	}
	if formState != nil {
		plan.ops = append(plan.ops, func() { restoreLiveFormState(existing, *formState) })
	}
	return nil
}

Parameters

parent js.Value
replacementParent js.Value

Returns

error
func (*patchPlan) planReplaceChildren(parent, replacementParent js.Value) error
{
	replacements := significantChildren(replacementParent)
	if _, err := indexIdentities(replacements); err != nil {
		return err
	}
	oldChildren := significantChildren(parent)
	plan.ops = append(plan.ops, func() {
		for _, child := range oldChildren {
			child.Call("remove")
		}
		for _, replacement := range replacements {
			clone := replacement.Call("cloneNode", true)
			parent.Call("appendChild", clone)
			recordRenderedTreeFromSource(clone, replacement)
		}
	})
	return nil
}

Parameters

existing js.Value
replacement js.Value
func (*patchPlan) planAttributes(existing, replacement js.Value)
{
	previous := renderedAttributes(existing)
	next := attributeSnapshot(replacement)
	names := make(map[string]struct{}, len(previous)+len(next))
	for name := range previous {
		names[name] = struct{}{}
	}
	for name := range next {
		names[name] = struct{}{}
	}
	for name := range names {
		previousValue, previouslyRendered := previous[name]
		nextValue, renderedNext := next[name]
		if !frameworkOwnedAttribute(name) && previouslyRendered == renderedNext && previousValue == nextValue {
			continue
		}
		if !renderedNext {
			if existing.Call("hasAttribute", name).Bool() {
				attrName := name
				plan.ops = append(plan.ops, func() { existing.Call("removeAttribute", attrName) })
			}
			continue
		}
		if !existing.Call("hasAttribute", name).Bool() || existing.Call("getAttribute", name).String() != nextValue {
			attrName, attrValue := name, nextValue
			plan.ops = append(plan.ops, func() { existing.Call("setAttribute", attrName, attrValue) })
		}
	}
	plan.ops = append(plan.ops, func() { setRenderedAttributes(existing, next) })
}
planChildren
Method

Parameters

parent js.Value
replacementParent js.Value

Returns

error
func (*patchPlan) planChildren(parent, replacementParent js.Value) error
{
	oldChildren := significantChildren(parent)
	newChildren := significantChildren(replacementParent)

	oldByIdentity, err := indexIdentities(oldChildren)
	if err != nil {
		return err
	}
	if _, err := indexIdentities(newChildren); err != nil {
		return err
	}

	consumed := make([]bool, len(oldChildren))
	placements := make([]childPlacement, 0, len(newChildren))
	nextUnkeyed := 0
	for _, replacement := range newChildren {
		identity := nodeIdentity(replacement)
		if identity != "" {
			if index, ok := oldByIdentity[identity]; ok {
				existing := oldChildren[index]
				if !samePatchType(existing, replacement) {
					return fmt.Errorf("rfw DOM patch: identity %q changed node type", identity)
				}
				consumed[index] = true
				if err := plan.planNode(existing, replacement); err != nil {
					return err
				}
				placements = append(placements, childPlacement{existing: existing})
			} else {
				placements = append(placements, childPlacement{source: replacement})
			}
			continue
		}

		for nextUnkeyed < len(oldChildren) && (consumed[nextUnkeyed] || nodeIdentity(oldChildren[nextUnkeyed]) != "") {
			nextUnkeyed++
		}
		if nextUnkeyed < len(oldChildren) && samePatchType(oldChildren[nextUnkeyed], replacement) {
			existing := oldChildren[nextUnkeyed]
			consumed[nextUnkeyed] = true
			nextUnkeyed++
			if err := plan.planNode(existing, replacement); err != nil {
				return err
			}
			placements = append(placements, childPlacement{existing: existing})
			continue
		}

		placements = append(placements, childPlacement{source: replacement})
	}

	plan.ops = append(plan.ops, func() {
		cursor := firstSignificantChild(parent)
		for index := range placements {
			placement := &placements[index]
			node := placement.existing
			if !node.Truthy() {
				node = placement.source.Call("cloneNode", true)
				recordRenderedTreeFromSource(node, placement.source)
			}
			if !cursor.Truthy() {
				parent.Call("appendChild", node)
			} else if node.Equal(cursor) {
				cursor = nextSignificantSibling(cursor)
			} else {
				parent.Call("insertBefore", node, cursor)
			}
		}
		for index, child := range oldChildren {
			if consumed[index] {
				continue
			}
			currentParent := child.Get("parentNode")
			if currentParent.Truthy() && currentParent.Equal(parent) {
				child.Call("remove")
			}
		}
	})
	return nil
}

Fields

Name Type Description
ownerID string
ops []func()
S
struct

childPlacement

dom/reconcile.go:22-25
type childPlacement struct

Fields

Name Type Description
existing js.Value
source js.Value
S
struct

liveFormState

dom/reconcile.go:27-38
type liveFormState struct

Fields

Name Type Description
value string
checked bool
selectedIndex int
selectionAt int
selectionEnd int
selectionDir string
hasValue bool
hasChecked bool
hasSelected bool
hasCaret bool
S
struct

activeFormState

dom/reconcile.go:40-43
type activeFormState struct

Methods

restore
Method
func (activeFormState) restore()
{
	if snapshot.state == nil || !snapshot.element.Truthy() || !snapshot.element.Get("isConnected").Bool() {
		return
	}
	active := js.Global().Get("document").Get("activeElement")
	if !active.Truthy() || !active.Equal(snapshot.element) {
		return
	}
	restoreLiveFormState(snapshot.element, *snapshot.state)
}

Fields

Name Type Description
element js.Value
state *liveFormState
F
function

captureActiveFormState

Parameters

root

Returns

dom/reconcile.go:45-51
func captureActiveFormState(root js.Value) activeFormState

{
	active := js.Global().Get("document").Get("activeElement")
	if !active.Truthy() || !root.Call("contains", active).Bool() {
		return activeFormState{}
	}
	return activeFormState{element: active, state: captureLiveFormState(active, active)}
}
F
function

patchInnerHTML

Parameters

element
html
string
dom/reconcile.go:64-81
func patchInnerHTML(element js.Value, html string)

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

	ownerID := attribute(element, "data-component-id")
	plan := &patchPlan{ownerID: ownerID}
	firstChild := newContent.Get("firstChild")
	if firstChild.Truthy() && firstChild.Get("nodeName").String() == "ROOT" &&
		ownerID != "" && attribute(firstChild, "data-component-id") == ownerID {
		if err := plan.planNode(element, firstChild); err != nil {
			panic(err)
		}
	} else if err := plan.planChildren(element, newContent); err != nil {
		panic(err)
	}
	plan.commit()
}
F
function

indexIdentities

Parameters

children

Returns

map[string]int
error
dom/reconcile.go:268-281
func indexIdentities(children []js.Value) (map[string]int, error)

{
	indexed := make(map[string]int)
	for index, child := range children {
		identity := nodeIdentity(child)
		if identity == "" {
			continue
		}
		if _, exists := indexed[identity]; exists {
			return nil, fmt.Errorf("rfw DOM patch: duplicate sibling identity %q", identity)
		}
		indexed[identity] = index
	}
	return indexed, nil
}
F
function

nodeIdentity

Parameters

node

Returns

string
dom/reconcile.go:283-313
func nodeIdentity(node js.Value) string

{
	if node.Get("nodeType").Int() != 1 {
		return ""
	}
	if key := attribute(node, "data-key"); key != "" {
		return "key:" + attribute(node, "data-for") + ":" + key
	}
	for _, candidate := range []struct {
		attribute string
		prefix    string
	}{
		{"data-component-id", "component:"},
		{"data-condition", "condition:"},
		{"data-for-anchor", "for-anchor:"},
		{"data-portal-id", "portal:"},
	} {
		if value := attribute(node, candidate.attribute); value != "" {
			return candidate.prefix + value
		}
	}
	if node.Call("hasAttribute", "data-router-outlet").Bool() {
		return "router-outlet"
	}
	if node.Call("hasAttribute", "data-portal-anchor").Bool() {
		return "portal-anchor"
	}
	if node.Call("hasAttribute", "data-keepalive-host").Bool() {
		return "keepalive-host"
	}
	return ""
}
F
function

samePatchType

Parameters

existing
replacement

Returns

bool
dom/reconcile.go:315-318
func samePatchType(existing, replacement js.Value) bool

{
	return existing.Get("nodeType").Int() == replacement.Get("nodeType").Int() &&
		existing.Get("nodeName").String() == replacement.Get("nodeName").String()
}
F
function

significantChildren

Parameters

parent

Returns

dom/reconcile.go:320-331
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

firstSignificantChild

Parameters

parent

Returns

dom/reconcile.go:333-335
func firstSignificantChild(parent js.Value) js.Value

{
	return nextSignificantNode(parent.Get("firstChild"))
}
F
function

nextSignificantSibling

Parameters

node

Returns

dom/reconcile.go:337-339
func nextSignificantSibling(node js.Value) js.Value

{
	return nextSignificantNode(node.Get("nextSibling"))
}
F
function

nextSignificantNode

Parameters

node

Returns

dom/reconcile.go:341-346
func nextSignificantNode(node js.Value) js.Value

{
	for node.Truthy() && node.Get("nodeType").Int() == 3 && strings.TrimSpace(node.Get("nodeValue").String()) == "" {
		node = node.Get("nextSibling")
	}
	return node
}
F
function

isRouterOutlet

Parameters

node

Returns

bool
dom/reconcile.go:348-350
func isRouterOutlet(node js.Value) bool

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

attribute

Parameters

node
name
string

Returns

string
dom/reconcile.go:352-357
func attribute(node js.Value, name string) string

{
	if node.Get("nodeType").Int() != 1 || !node.Call("hasAttribute", name).Bool() {
		return ""
	}
	return node.Call("getAttribute", name).String()
}
F
function

attributeSnapshot

Parameters

node

Returns

map[string]string
dom/reconcile.go:359-370
func attributeSnapshot(node js.Value) map[string]string

{
	attributes := make(map[string]string)
	if node.Get("nodeType").Int() != 1 {
		return attributes
	}
	names := node.Call("getAttributeNames")
	for i := 0; i < names.Length(); i++ {
		name := names.Index(i).String()
		attributes[name] = node.Call("getAttribute", name).String()
	}
	return attributes
}
F
function

renderedAttributes

Parameters

node

Returns

map[string]string
dom/reconcile.go:372-384
func renderedAttributes(node js.Value) map[string]string

{
	rendered := node.Get(renderedAttrsProperty)
	if rendered.Type() != js.TypeObject {
		return attributeSnapshot(node)
	}
	attributes := make(map[string]string)
	keys := js.Object().Call("keys", rendered)
	for i := 0; i < keys.Length(); i++ {
		name := keys.Index(i).String()
		attributes[name] = rendered.Get(name).String()
	}
	return attributes
}
F
function

setRenderedAttributes

Parameters

node
attributes
map[string]string
dom/reconcile.go:386-392
func setRenderedAttributes(node js.Value, attributes map[string]string)

{
	rendered := js.NewDict()
	for name, value := range attributes {
		rendered.Set(name, value)
	}
	node.Set(renderedAttrsProperty, rendered.Value)
}
F
function

recordRenderedTree

Parameters

root
dom/reconcile.go:394-402
func recordRenderedTree(root js.Value)

{
	if root.Get("nodeType").Int() == 1 {
		setRenderedAttributes(root, attributeSnapshot(root))
	}
	children := root.Get("childNodes")
	for i := 0; i < children.Length(); i++ {
		recordRenderedTree(children.Index(i))
	}
}
F
function

recordRenderedTreeFromSource

Parameters

node
source
dom/reconcile.go:404-417
func recordRenderedTreeFromSource(node, source js.Value)

{
	if node.Get("nodeType").Int() == 1 && source.Get("nodeType").Int() == 1 {
		setRenderedAttributes(node, attributeSnapshot(source))
	}
	nodeChildren := node.Get("childNodes")
	sourceChildren := source.Get("childNodes")
	limit := nodeChildren.Length()
	if sourceChildren.Length() < limit {
		limit = sourceChildren.Length()
	}
	for i := 0; i < limit; i++ {
		recordRenderedTreeFromSource(nodeChildren.Index(i), sourceChildren.Index(i))
	}
}
F
function

frameworkOwnedAttribute

Parameters

name
string

Returns

bool
dom/reconcile.go:419-427
func frameworkOwnedAttribute(name string) bool

{
	return strings.HasPrefix(name, "data-component-") ||
		strings.HasPrefix(name, "data-key") ||
		strings.HasPrefix(name, "data-for") ||
		strings.HasPrefix(name, "data-condition") ||
		strings.HasPrefix(name, "data-router-") ||
		strings.HasPrefix(name, "data-bind-") ||
		strings.HasPrefix(name, "data-on-")
}
F
function

captureLiveFormState

Parameters

existing
replacement

Returns

dom/reconcile.go:429-472
func captureLiveFormState(existing, replacement js.Value) *liveFormState

{
	controlled := isControlledForm(existing) || isControlledForm(replacement)
	tag := existing.Get("nodeName").String()
	state := &liveFormState{}
	switch tag {
	case "INPUT":
		if !controlled {
			state.value = existing.Get("value").String()
			state.hasValue = true
		}
		typ := strings.ToLower(existing.Get("type").String())
		if !controlled && (typ == "checkbox" || typ == "radio") {
			state.checked = existing.Get("checked").Bool()
			state.hasChecked = true
		}
	case "TEXTAREA":
		if !controlled {
			state.value = existing.Get("value").String()
			state.hasValue = true
		}
	case "SELECT":
		if !controlled {
			state.selectedIndex = existing.Get("selectedIndex").Int()
			state.hasSelected = true
		}
	default:
		return nil
	}
	active := js.Global().Get("document").Get("activeElement")
	if active.Truthy() && active.Equal(existing) {
		start := existing.Get("selectionStart")
		end := existing.Get("selectionEnd")
		if start.Type() == js.TypeNumber && end.Type() == js.TypeNumber {
			state.selectionAt = start.Int()
			state.selectionEnd = end.Int()
			direction := existing.Get("selectionDirection")
			if direction.Type() == js.TypeString {
				state.selectionDir = direction.String()
			}
			state.hasCaret = true
		}
	}
	return state
}
F
function

restoreLiveFormState

Parameters

element
dom/reconcile.go:474-494
func restoreLiveFormState(element js.Value, state liveFormState)

{
	if state.hasValue && element.Get("value").String() != state.value {
		element.Set("value", state.value)
	}
	if state.hasChecked && element.Get("checked").Bool() != state.checked {
		element.Set("checked", state.checked)
	}
	if state.hasSelected && element.Get("selectedIndex").Int() != state.selectedIndex {
		element.Set("selectedIndex", state.selectedIndex)
	}
	if state.hasCaret {
		setter := element.Get("setSelectionRange")
		if setter.Type() == js.TypeFunction {
			if state.selectionDir != "" {
				element.Call("setSelectionRange", state.selectionAt, state.selectionEnd, state.selectionDir)
			} else {
				element.Call("setSelectionRange", state.selectionAt, state.selectionEnd)
			}
		}
	}
}
F
function

isControlledForm

Parameters

node

Returns

bool
dom/reconcile.go:496-510
func isControlledForm(node js.Value) bool

{
	if node.Get("nodeType").Int() != 1 {
		return false
	}
	if node.Call("hasAttribute", "data-bind-store").Bool() || node.Call("hasAttribute", "data-bind-signal").Bool() {
		return true
	}
	for _, name := range []string{"value", "checked"} {
		value := attribute(node, name)
		if strings.Contains(value, ":w") && (strings.Contains(value, "@store:") || strings.Contains(value, "@signal:")) {
			return true
		}
	}
	return false
}
F
function

BenchmarkPatchKeyedList

Parameters

dom/reconcile_benchmark_test.go:12-35
func BenchmarkPatchKeyedList(b *testing.B)

{
	for _, size := range []int{10, 100, 1000} {
		b.Run(strconv.Itoa(size), func(b *testing.B) {
			root := CreateElement("root")
			componentID := "benchmark-keyed-" + strconv.Itoa(size)
			root.SetAttr("data-component-id", componentID)
			forward := benchmarkListHTML(componentID, size, false)
			reverse := benchmarkListHTML(componentID, size, true)
			root.SetHTML(strings.TrimSuffix(strings.TrimPrefix(forward, `<root data-component-id="`+componentID+`">`), "</root>"))
			recordRenderedTree(root.Value)
			Doc().Body().AppendChild(root)
			b.Cleanup(func() { root.Call("remove") })

			b.ResetTimer()
			for i := 0; i < b.N; i++ {
				if i%2 == 0 {
					patchInnerHTML(root.Value, reverse)
				} else {
					patchInnerHTML(root.Value, forward)
				}
			}
		})
	}
}
F
function

benchmarkListHTML

Parameters

componentID
string
size
int
reverse
bool

Returns

string
dom/reconcile_benchmark_test.go:37-49
func benchmarkListHTML(componentID string, size int, reverse bool) string

{
	var html strings.Builder
	fmt.Fprintf(&html, `<root data-component-id="%s"><ul>`, componentID)
	for position := 0; position < size; position++ {
		item := position
		if reverse {
			item = size - position - 1
		}
		fmt.Fprintf(&html, `<li data-for="benchmark" data-key="%d">row-%d</li>`, item, item)
	}
	html.WriteString(`</ul></root>`)
	return html.String()
}
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

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

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")
	}
}
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

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

TestGlobalHandlerRunsOnceAcrossNestedDelegates

Parameters

dom/handlers_test.go:51-72
func TestGlobalHandlerRunsOnceAcrossNestedDelegates(t *testing.T)

{
	root := mountHandlerRoot(t, `<div id="nested"><button data-on-click="page">next</button></div>`)
	nested := root.Query("#nested")
	calls := 0

	RegisterHandlerFunc("page", func() { calls++ })
	DelegateEvents("outer", root.Value)
	DelegateEvents("inner", nested.Value)
	t.Cleanup(func() {
		RemoveDelegatedEvents("inner", nested.Value)
		RemoveDelegatedEvents("outer", root.Value)
	})

	nested.Query("button").Call("click")
	if calls != 1 {
		t.Fatalf("global handler calls = %d, want 1", calls)
	}
	nested.Query("button").Call("click")
	if calls != 2 {
		t.Fatalf("global handler calls after a second click = %d, want 2", calls)
	}
}
F
function

TestNestedDelegatesContinuePastClaimedBinding

Parameters

dom/handlers_test.go:74-92
func TestNestedDelegatesContinuePastClaimedBinding(t *testing.T)

{
	root := mountHandlerRoot(t, `<div id="nested" data-on-click="parent"><button data-on-click="child">next</button></div>`)
	nested := root.Query("#nested")
	var childCalls, parentCalls int

	RegisterHandlerFunc("child", func() { childCalls++ })
	RegisterHandlerFunc("parent", func() { parentCalls++ })
	DelegateEvents("outer-ancestor", root.Value)
	DelegateEvents("inner-ancestor", nested.Value)
	t.Cleanup(func() {
		RemoveDelegatedEvents("inner-ancestor", nested.Value)
		RemoveDelegatedEvents("outer-ancestor", root.Value)
	})

	nested.Query("button").Call("click")
	if childCalls != 1 || parentCalls != 1 {
		t.Fatalf("handler calls = child %d, parent %d; want 1, 1", childCalls, parentCalls)
	}
}
F
function

TestDelegatedEventModifiers

Parameters

dom/handlers_test.go:94-113
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:115-157
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:159-175
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)
	}
}
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)
		}
	}
}
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

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 }
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")
	}
}