dom
packageAPI reference for the dom
package.
Imports
(12)TestNullElementIsInert
A null element (missing query result) must be inert: mutators no-op and
readers return zero values instead of panicking.
Parameters
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")
}
}
TestElementAttrsAndStyle
Parameters
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)
}
}
TestElementCollections
Parameters
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")
}
}
TestElementAppendChild
Parameters
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")
}
}
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
Returns
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()
}
TestStyleInline
Parameters
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)
}
}
BindStoreInputsForComponent
BindStoreInputsForComponent is a no-op outside wasm builds.
Parameters
func BindStoreInputsForComponent(string, any)
{}
BindStoreInputs
BindStoreInputs is a no-op outside wasm builds.
Parameters
func BindStoreInputs(any)
{}
SnapshotComponentSignals
SnapshotComponentSignals is a stub returning nil outside wasm builds.
Parameters
Returns
func SnapshotComponentSignals(string) map[string]any
{ return nil }
Element
Element wraps a DOM element and provides typed helpers.
type Element struct
Methods
Query returns the first descendant matching the CSS selector.
Parameters
Returns
func (Element) Query(sel string) Element
{
return Element{e.Call("querySelector", sel)}
}
QueryAll returns all descendants matching the selector.
Parameters
Returns
func (Element) QueryAll(sel string) Element
{
return Element{e.Call("querySelectorAll", sel)}
}
ByClass returns all descendants with the given class name.
Parameters
Returns
func (Element) ByClass(name string) Element
{
return Element{e.Call("getElementsByClassName", name)}
}
ByTag returns all descendants with the given tag name.
Parameters
Returns
func (Element) ByTag(tag string) Element
{
return Element{e.Call("getElementsByTagName", tag)}
}
Text returns the element's text content.
Returns
func (Element) Text() string
{
if e.missing() {
return ""
}
return e.Get("textContent").String()
}
SetText sets the element's text content.
Parameters
func (Element) SetText(txt string)
{
if e.missing() {
return
}
e.Set("textContent", txt)
}
HTML returns the element's inner HTML.
Returns
func (Element) HTML() string
{
if e.missing() {
return ""
}
return e.Get("innerHTML").String()
}
SetHTML replaces the element's children with raw HTML.
Parameters
func (Element) SetHTML(html string)
{
if e.missing() {
return
}
e.Set("innerHTML", html)
}
AppendChild appends a child element.
Parameters
func (Element) AppendChild(child Element)
{
if e.missing() {
return
}
e.Call("appendChild", child.Value)
}
Attr retrieves the value of an attribute or "" if unset.
Parameters
Returns
func (Element) Attr(name string) string
{
if e.missing() {
return ""
}
v := e.Call("getAttribute", name)
if v.Truthy() {
return v.String()
}
return ""
}
SetAttr sets the value of an attribute on the element.
Parameters
func (Element) SetAttr(name, value string)
{
if e.missing() {
return
}
e.Call("setAttribute", name, value)
}
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
func (Element) RemoveAttr(name string)
{
if e.missing() {
return
}
e.Call("removeAttribute", name)
}
Matches reports whether the element itself satisfies the selector, the non-walking counterpart of Closest.
Parameters
Returns
func (Element) Matches(sel string) bool
{
if e.missing() {
return false
}
return e.Call("matches", sel).Bool()
}
SetStyle sets an inline style property on the element.
Parameters
func (Element) SetStyle(prop, value string)
{
if e.missing() {
return
}
e.Get("style").Call("setProperty", prop, value)
}
AddClass adds a class to the element.
Parameters
func (Element) AddClass(name string)
{
if e.missing() {
return
}
e.Get("classList").Call("add", name)
}
RemoveClass removes a class from the element.
Parameters
func (Element) RemoveClass(name string)
{
if e.missing() {
return
}
e.Get("classList").Call("remove", name)
}
HasClass reports whether the element has the given class.
Parameters
Returns
func (Element) HasClass(name string) bool
{
if e.missing() {
return false
}
return e.Get("classList").Call("contains", name).Bool()
}
ToggleClass toggles the presence of a class on the element.
Parameters
func (Element) ToggleClass(name string)
{
if e.missing() {
return
}
e.Get("classList").Call("toggle", name)
}
Length returns the number of children when the element represents a collection.
Returns
func (Element) Length() int
{ return e.Get("length").Int() }
Index retrieves the element at the given position when representing a collection.
Parameters
Returns
func (Element) Index(i int) Element
{ return Element{e.Value.Index(i)} }
Val returns the element's value property (inputs, selects, textareas). Named Val because the embedded js.Value field occupies Value.
Returns
func (Element) Val() string
{
if e.missing() {
return ""
}
return e.Get("value").String()
}
SetValue sets the element's value property.
Parameters
func (Element) SetValue(v string)
{
if e.missing() {
return
}
e.Set("value", v)
}
Checked reports whether a checkbox or radio input is checked.
Returns
func (Element) Checked() bool
{
if e.missing() {
return false
}
return e.Get("checked").Bool()
}
Data reads a data-* attribute by its dataset key (camelCase: data-item-id becomes Data("itemId")).
Parameters
Returns
func (Element) Data(key string) string
{
if e.missing() {
return ""
}
v := e.Get("dataset").Get(key)
if !v.Truthy() {
return ""
}
return v.String()
}
Closest returns the nearest ancestor (or the element itself) matching the selector; check IsNull on the result for no match.
Parameters
Returns
func (Element) Closest(sel string) Element
{
if e.missing() {
return e
}
return Element{e.Call("closest", sel)}
}
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
func (Element) missing() bool
{ return e.IsNull() || e.IsUndefined() }
On attaches a listener for event to the element and returns a stop function.
Parameters
Returns
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 attaches a click handler to the element.
Parameters
Returns
func (Element) OnClick(handler func(Event)) func()
{
return e.On("click", handler)
}
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
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)
}
RegisterComponentHandler
RegisterComponentHandler registers a handler owned by one component instance.
Parameters
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)
}
RegisterHandlerFunc
RegisterHandlerFunc registers a no-argument Go function in the handler registry.
Parameters
func RegisterHandlerFunc(name string, fn func())
{
RegisterHandler(name, func(_ js.Value, _ []js.Value) any {
fn()
return nil
})
}
RegisterComponentHandlerFunc
RegisterComponentHandlerFunc registers a no-argument component handler.
Parameters
func RegisterComponentHandlerFunc(componentID, name string, fn func())
{
RegisterComponentHandler(componentID, name, func(_ js.Value, _ []js.Value) any {
fn()
return nil
})
}
RegisterHandlerEvent
RegisterHandlerEvent registers a Go function that receives the first argument as an event object.
Parameters
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
})
}
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
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
})
}
GetHandler
GetHandler retrieves a registered handler by name.
Parameters
Returns
func GetHandler(name string) js.Func
{
handlerMu.RLock()
defer handlerMu.RUnlock()
if v, ok := handlerRegistry[name]; ok {
return v
}
return js.Func{}
}
GetComponentHandler
GetComponentHandler resolves a component handler before the global fallback.
Parameters
Returns
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]
}
ReleaseComponentHandlers
ReleaseComponentHandlers releases every handler owned by a component.
Parameters
func ReleaseComponentHandlers(componentID string)
{
handlerMu.Lock()
handlers := componentHandlerRegistry[componentID]
delete(componentHandlerRegistry, componentID)
handlerMu.Unlock()
for _, handler := range handlers {
handler.Release()
}
}
delegatedHandler
type delegatedHandler struct
Fields
| Name | Type | Description |
|---|---|---|
| event | string | |
| capture | bool | |
| fn | js.Func | |
| stop | func() |
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
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()
}
newDelegatedHandler
Parameters
Returns
func newDelegatedHandler(componentID string, root js.Value, event string, capture bool) delegatedHandler
{
var timerMu sync.Mutex
timers := make(map[string]*time.Timer)
throttled := make(map[string]time.Time)
fn := js.SafeFuncOf(func(_ js.Value, args []js.Value) any {
if len(args) == 0 {
return nil
}
evt := args[0]
target := evt.Get("target")
for target.Truthy() {
handlerName := target.Call("getAttribute", "data-on-"+event)
if handlerName.Truthy() {
modifiers := eventModifiers(target, event)
_, wantsCapture := modifiers["capture"]
nonBubbling := event == "focus" || event == "blur"
if (nonBubbling || wantsCapture == capture) && eventAllowed(evt, target, modifiers) {
h := GetComponentHandler(componentID, handlerName.String())
if h.Truthy() {
if _, ok := modifiers["prevent"]; ok {
if _, passive := modifiers["passive"]; !passive {
evt.Call("preventDefault")
}
}
if _, ok := modifiers["stop"]; ok {
evt.Call("stopPropagation")
}
if _, ok := modifiers["once"]; ok {
target.Call("removeAttribute", "data-on-"+event)
target.Call("removeAttribute", "data-on-"+event+"-modifiers")
}
invoke := func() {
defer func() {
if r := recover(); r != nil && OnHandlerPanic != nil {
OnHandlerPanic(r, handlerName.String())
}
}()
h.Invoke(evt, target)
}
key := eventBindingKey(target, event, handlerName.String())
if delay, ok := modifierDelay(modifiers, "debounce"); ok {
timerMu.Lock()
if timer := timers[key]; timer != nil {
timer.Stop()
}
var scheduled *time.Timer
scheduled = time.AfterFunc(delay, func() {
invoke()
timerMu.Lock()
if timers[key] == scheduled {
delete(timers, key)
}
timerMu.Unlock()
})
timers[key] = scheduled
timerMu.Unlock()
return nil
}
if delay, ok := modifierDelay(modifiers, "throttle"); ok {
timerMu.Lock()
last := throttled[key]
if time.Since(last) < delay {
timerMu.Unlock()
return nil
}
throttled[key] = time.Now()
timerMu.Unlock()
}
invoke()
return nil
}
}
}
if target.Equal(root) {
break
}
target = target.Get("parentElement")
}
return nil
})
stop := func() {
timerMu.Lock()
for _, timer := range timers {
timer.Stop()
}
clear(timers)
clear(throttled)
timerMu.Unlock()
}
return delegatedHandler{event: event, capture: capture, fn: fn, stop: stop}
}
eventModifiers
Parameters
Returns
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
}
eventAllowed
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
}
modifierDelay
Parameters
Returns
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
}
eventBindingKey
Parameters
Returns
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
}
RemoveDelegatedEvents
RemoveDelegatedEvents removes all delegated event listeners for the given component.
Parameters
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()
}
}
Document
Document wraps the global document object.
type Document struct
Methods
ByID fetches an element by id.
Parameters
Returns
func (Document) ByID(id string) Element
{
if !d.Truthy() {
return Element{js.Null()}
}
return Element{d.Call("getElementById", id)}
}
Query returns the first element matching the selector.
Parameters
Returns
func (Document) Query(sel string) Element
{
if !d.Truthy() {
return Element{js.Null()}
}
return Element{d.Call("querySelector", sel)}
}
QueryAll returns all elements matching the selector.
Parameters
Returns
func (Document) QueryAll(sel string) Element
{
if !d.Truthy() {
return Element{js.Null()}
}
return Element{d.Call("querySelectorAll", sel)}
}
ByClass returns all elements with the given class name.
Parameters
Returns
func (Document) ByClass(name string) Element
{
return Element{d.Call("getElementsByClassName", name)}
}
ByTag returns all elements with the given tag name.
Parameters
Returns
func (Document) ByTag(tag string) Element
{
return Element{d.Call("getElementsByTagName", tag)}
}
CreateElement creates a new element with the tag.
Parameters
Returns
func (Document) CreateElement(tag string) Element
{
return Element{d.Call("createElement", tag)}
}
Head returns the document's <head> element.
Returns
func (Document) Head() Element
{ return Element{d.Get("head")} }
Doc
Doc returns the global Document.
Returns
func Doc() Document
{ return Document{js.Doc()} }
Uses
addInputBindingStop
Parameters
func addInputBindingStop(componentID string, stop func())
{
inputBindingStopsMu.Lock()
inputBindingStops[componentID] = append(inputBindingStops[componentID], stop)
inputBindingStopsMu.Unlock()
}
ReleaseInputBindings
ReleaseInputBindings stops all input listeners registered for a component.
UpdateDOM calls it before rebinding and core calls it on unmount.
Parameters
func ReleaseInputBindings(componentID string)
{
inputBindingStopsMu.Lock()
stops := inputBindingStops[componentID]
delete(inputBindingStops, componentID)
inputBindingStopsMu.Unlock()
for _, stop := range stops {
stop()
}
}
RegisterSignal
RegisterSignal associates a signal with a component so inputs can bind to it.
Parameters
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()
}
RemoveComponentSignals
RemoveComponentSignals cleans up signals for a component on unmount.
Parameters
func RemoveComponentSignals(componentID string)
{
componentSignalsMu.Lock()
delete(componentSignals, componentID)
componentSignalsMu.Unlock()
}
getSignal
Parameters
Returns
func getSignal(componentID, name string) any
{
componentSignalsMu.RLock()
defer componentSignalsMu.RUnlock()
if m, ok := componentSignals[componentID]; ok {
return m[name]
}
return nil
}
SnapshotComponentSignals
SnapshotComponentSignals returns a copy of the signals registered for a component.
Parameters
Returns
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
}
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
Returns
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
}
Uses
UpdateDOM
UpdateDOM patches the DOM of the specified component with the provided
HTML string, resolving the target via typed Document/Element wrappers.
Parameters
func UpdateDOM(componentID string, html string)
{
element := ComponentRoot(componentID)
if element.IsNull() || element.IsUndefined() {
return
}
// Diff-patch only when the resolved element is the component's OWN root: that
// is an in-place reactive update, where patching preserves focus/selection.
// Otherwise the target is the #app fallback (a fresh mount or a route change,
// since ComponentRoot falls back to #app when the component root is not yet
// in the DOM). There, positionally diffing two different <root> trees leaves
// stale nodes from the previous component, so replace wholesale instead.
elID := element.Call("getAttribute", "data-component-id")
if componentID != "" && elID.Truthy() && elID.String() == componentID {
patchInnerHTML(element.Value, html)
} else {
element.Set("innerHTML", html)
}
if TemplateHook != nil {
TemplateHook(componentID, html)
}
// Release the listeners of the previous render: rebinding below attaches
// fresh ones and stale listeners on replaced nodes would leak.
ReleaseInputBindings(componentID)
BindStoreInputsForComponent(componentID, element.Value)
BindSignalInputs(componentID, element.Value)
BindASTStoreInputs(componentID, element.Value)
BindASTSignalInputs(componentID, element.Value)
UpdateLifecycleHooks(componentID)
}
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
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)
}
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
func UpdateDOMIn(target Element, componentID, html string)
{
if target.IsNull() || target.IsUndefined() {
return
}
target.Set("innerHTML", html)
if TemplateHook != nil {
TemplateHook(componentID, html)
}
ReleaseInputBindings(componentID)
BindStoreInputsForComponent(componentID, target.Value)
BindSignalInputs(componentID, target.Value)
BindASTStoreInputs(componentID, target.Value)
BindASTSignalInputs(componentID, target.Value)
UpdateLifecycleHooks(componentID)
}
Uses
BindASTStoreInputs
BindASTStoreInputs binds input elements that have data-bind-store attributes
(emitted by the AST renderer) to their store variables.
Parameters
func BindASTStoreInputs(componentID string, element js.Value)
{
inputs := element.Call("querySelectorAll", "[data-bind-store]")
for i := 0; i < inputs.Length(); i++ {
input := inputs.Index(i)
binding := input.Call("getAttribute", "data-bind-store").String()
parts := strings.Split(binding, ".")
if len(parts) != 3 {
continue
}
module, storeName, key := parts[0], parts[1], parts[2]
store := state.GlobalStoreManager.GetStore(module, storeName)
if store == nil {
continue
}
if StoreBindingHook != nil && componentID != "" {
StoreBindingHook(componentID, module, storeName, key)
}
storeValue := store.Get(key)
tag := strings.ToLower(input.Get("tagName").String())
if tag == "input" {
inputType := input.Get("type").String()
if inputType == "checkbox" {
if b, ok := storeValue.(bool); ok {
input.Set("checked", b)
}
ch, stop := events.Listen("change", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, st *state.Store, k string) {
for range ch {
st.Set(k, in.Get("checked").Bool())
}
}(input, store, key)
continue
}
}
if storeValue == nil {
storeValue = ""
}
input.Set("value", fmt.Sprintf("%v", storeValue))
ch, stop := events.Listen("input", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, st *state.Store, k string) {
for range ch {
st.Set(k, in.Get("value").String())
}
}(input, store, key)
}
}
BindASTSignalInputs
BindASTSignalInputs binds input elements that have data-bind-signal attributes
(emitted by the AST renderer) to their signals.
Parameters
func BindASTSignalInputs(componentID string, element js.Value)
{
inputs := element.Call("querySelectorAll", "[data-bind-signal]")
for i := 0; i < inputs.Length(); i++ {
input := inputs.Index(i)
name := input.Call("getAttribute", "data-bind-signal").String()
sig := getSignal(componentID, name)
if sig == nil {
continue
}
tag := strings.ToLower(input.Get("tagName").String())
if tag == "input" {
inputType := input.Get("type").String()
if inputType == "checkbox" {
if s, ok := sig.(interface {
Read() any
Set(bool)
}); ok {
if b, ok := s.Read().(bool); ok {
input.Set("checked", b)
}
ch, stop := events.Listen("change", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, sg interface{ Set(bool) }) {
for range ch {
sg.Set(in.Get("checked").Bool())
}
}(input, s)
continue
}
}
}
if s, ok := sig.(interface {
Read() any
Set(string)
}); ok {
input.Set("value", fmt.Sprintf("%v", s.Read()))
ch, stop := events.Listen("input", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, sg interface{ Set(string) }) {
for range ch {
sg.Set(in.Get("value").String())
}
}(input, s)
}
}
}
BindStoreInputsForComponent
BindStoreInputsForComponent binds input elements to store variables while
providing the component context for runtime hooks.
Parameters
func BindStoreInputsForComponent(componentID string, element js.Value)
{
inputs := element.Call("querySelectorAll", "input, select, textarea")
for i := 0; i < inputs.Length(); i++ {
input := inputs.Index(i)
valueAttr := ""
if input.Call("hasAttribute", "value").Bool() {
valueAttr = input.Call("getAttribute", "value").String()
}
checkedAttr := ""
if input.Call("hasAttribute", "checked").Bool() {
checkedAttr = input.Call("getAttribute", "checked").String()
}
re := reStoreWrite
valueMatch := re.FindStringSubmatch(valueAttr)
checkedMatch := re.FindStringSubmatch(checkedAttr)
var module, storeName, key string
var usesChecked bool
if len(valueMatch) == 4 {
module, storeName, key = valueMatch[1], valueMatch[2], valueMatch[3]
} else if len(checkedMatch) == 4 {
module, storeName, key = checkedMatch[1], checkedMatch[2], checkedMatch[3]
usesChecked = true
} else {
continue
}
store := state.GlobalStoreManager.GetStore(module, storeName)
if store == nil {
continue
}
if StoreBindingHook != nil && componentID != "" {
StoreBindingHook(componentID, module, storeName, key)
}
storeValue := store.Get(key)
if usesChecked {
boolVal, _ := storeValue.(bool)
input.Set("checked", boolVal)
ch, stop := events.Listen("change", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, st *state.Store, k string) {
for range ch {
st.Set(k, in.Get("checked").Bool())
}
}(input, store, key)
continue
}
if storeValue == nil {
storeValue = ""
}
input.Set("value", fmt.Sprintf("%v", storeValue))
ch, stop := events.Listen("input", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, st *state.Store, k string) {
for range ch {
st.Set(k, in.Get("value").String())
}
}(input, store, key)
}
}
BindStoreInputs
BindStoreInputs binds input elements to store variables.
Parameters
func BindStoreInputs(element js.Value)
{
BindStoreInputsForComponent("", element)
}
BindSignalInputs
BindSignalInputs binds input elements to local component signals.
Parameters
func BindSignalInputs(componentID string, element js.Value)
{
inputs := element.Call("querySelectorAll", "input, select, textarea")
for i := 0; i < inputs.Length(); i++ {
input := inputs.Index(i)
valueAttr := ""
if input.Call("hasAttribute", "value").Bool() {
valueAttr = input.Call("getAttribute", "value").String()
}
checkedAttr := ""
if input.Call("hasAttribute", "checked").Bool() {
checkedAttr = input.Call("getAttribute", "checked").String()
}
re := reSignalWrite
valueMatch := re.FindStringSubmatch(valueAttr)
checkedMatch := re.FindStringSubmatch(checkedAttr)
var name string
var usesChecked bool
if len(valueMatch) == 2 {
name = valueMatch[1]
} else if len(checkedMatch) == 2 {
name = checkedMatch[1]
usesChecked = true
} else {
continue
}
sig := getSignal(componentID, name)
if sig == nil {
continue
}
if usesChecked {
if s, ok := sig.(interface {
Read() any
Set(bool)
}); ok {
if b, ok := s.Read().(bool); ok {
input.Set("checked", b)
}
ch, stop := events.Listen("change", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, sg interface{ Set(bool) }) {
for range ch {
sg.Set(in.Get("checked").Bool())
}
}(input, s)
}
continue
}
if s, ok := sig.(interface {
Read() any
Set(string)
}); ok {
input.Set("value", fmt.Sprintf("%v", s.Read()))
ch, stop := events.Listen("input", input)
addInputBindingStop(componentID, stop)
go func(in js.Value, sg interface{ Set(string) }) {
for range ch {
sg.Set(in.Get("value").String())
}
}(input, s)
}
}
}
patchInnerHTML
Parameters
func patchInnerHTML(element js.Value, html string)
{
activeEl := js.Global().Get("document").Get("activeElement")
activeID := ""
activeSelStart := 0
activeSelEnd := 0
if activeEl.Truthy() {
tag := activeEl.Get("nodeName").String()
if tag == "INPUT" || tag == "TEXTAREA" || tag == "SELECT" {
activeID = activeEl.Get("id").String()
if activeID != "" {
activeSelStart = activeEl.Get("selectionStart").Int()
activeSelEnd = activeEl.Get("selectionEnd").Int()
}
}
}
template := CreateElement("template")
template.Set("innerHTML", html)
newContent := template.Get("content")
patched := false
firstChild := newContent.Get("firstChild")
if firstChild.Truthy() && firstChild.Get("nodeName").String() == "ROOT" {
cid := element.Call("getAttribute", "data-component-id")
newCid := firstChild.Call("getAttribute", "data-component-id")
if cid.Truthy() && cid.String() == newCid.String() {
patchAttributes(element, firstChild)
patchChildren(element, firstChild)
patched = true
}
}
if !patched {
patchChildren(element, newContent)
}
if activeID != "" {
restore := js.Global().Get("document").Call("getElementById", activeID)
if restore.Truthy() {
restore.Call("focus")
restore.Call("setSelectionRange", activeSelStart, activeSelEnd)
}
}
}
patchChildren
func patchChildren(oldParent, newParent js.Value)
{
// Snapshot the significant children (elements and non-blank text).
// Whitespace-only text nodes are formatting noise: pairing them
// positionally shifts the diff whenever a keyed list grows or a
// conditional toggles, morphing unrelated siblings into each other.
oldKids := significantChildren(oldParent)
newKids := significantChildren(newParent)
keyed := make(map[string]js.Value)
for _, child := range oldKids {
if key := getDataKey(child); key != "" {
keyed[key] = child
}
}
consumed := make([]bool, len(oldKids))
oi := 0
// cursor returns the first unconsumed old node: inserts anchor before it.
cursor := func() js.Value {
for i := oi; i < len(oldKids); i++ {
if !consumed[i] {
return oldKids[i]
}
}
return js.Null()
}
insertAtCursor := func(node js.Value) {
if ref := cursor(); ref.Truthy() {
oldParent.Call("insertBefore", node, ref)
} else {
oldParent.Call("appendChild", node)
}
}
for _, newChild := range newKids {
if key := getDataKey(newChild); key != "" {
if oldChild, ok := keyed[key]; ok {
patchNode(oldChild, newChild)
if ref := cursor(); !oldChild.Equal(ref) {
insertAtCursor(oldChild)
} else {
// already in position: consume it
for i := oi; i < len(oldKids); i++ {
if oldKids[i].Equal(oldChild) {
consumed[i] = true
break
}
}
}
delete(keyed, key)
} else {
insertAtCursor(newChild.Call("cloneNode", true))
}
continue
}
// advance past keyed leftovers (handled through the map above)
for oi < len(oldKids) && (consumed[oi] || getDataKey(oldKids[oi]) != "") {
oi++
}
if oi < len(oldKids) && samePatchType(oldKids[oi], newChild) {
patchNode(oldKids[oi], newChild)
consumed[oi] = true
oi++
} else if oi < len(oldKids) {
oldParent.Call("replaceChild", newChild.Call("cloneNode", true), oldKids[oi])
consumed[oi] = true
oi++
} else {
oldParent.Call("appendChild", newChild.Call("cloneNode", true))
}
}
// leftover keyed nodes not reused by the new render
for _, child := range keyed {
child.Call("remove")
}
// leftover unkeyed significant nodes past the new list
for i := 0; i < len(oldKids); i++ {
if !consumed[i] && getDataKey(oldKids[i]) == "" {
oldKids[i].Call("remove")
}
}
}
significantChildren
significantChildren returns the child nodes that participate in diffing:
elements and text nodes with non-whitespace content.
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
}
samePatchType
samePatchType reports whether two nodes may be patched in place: same node
name and, for conditional wrappers, the same data-condition identity (a
wrapper morphing into an unrelated sibling emptied whole sections).
func samePatchType(oldNode, newNode js.Value) bool
{
if oldNode.Get("nodeName").String() != newNode.Get("nodeName").String() {
return false
}
if oldNode.Get("nodeType").Int() != 1 {
return true
}
oc := oldNode.Call("getAttribute", "data-condition")
nc := newNode.Call("getAttribute", "data-condition")
os, ns := "", ""
if !oc.IsNull() {
os = oc.String()
}
if !nc.IsNull() {
ns = nc.String()
}
return os == ns
}
getDataKey
Parameters
Returns
func getDataKey(node js.Value) string
{
if node.Get("nodeType").Int() != 1 {
return ""
}
key := node.Call("getAttribute", "data-key")
if key.Truthy() {
return key.String()
}
return ""
}
patchNode
func patchNode(oldNode, newNode js.Value)
{
nodeType := newNode.Get("nodeType").Int()
// The router owns whatever sits inside an outlet. A shell that re-renders
// (a store-driven list or condition in a MountRoot root) must not diff the
// routed page away, and must not undo the DOM a mounted page built for
// itself after its own render.
if nodeType == 1 && isRouterOutlet(oldNode) && isRouterOutlet(newNode) {
patchAttributes(oldNode, newNode)
return
}
if nodeType == 3 { // Text node
if oldNode.Get("nodeValue").String() != newNode.Get("nodeValue").String() {
oldNode.Set("nodeValue", newNode.Get("nodeValue"))
}
return
}
if oldNode.Get("nodeName").String() != newNode.Get("nodeName").String() {
oldNode.Call("replaceWith", newNode.Call("cloneNode", true))
return
}
if nodeType == 1 { // Element node
patchAttributes(oldNode, newNode)
}
patchChildren(oldNode, newNode)
}
isRouterOutlet
isRouterOutlet reports whether the node is the router’s outlet marker.
Parameters
Returns
func isRouterOutlet(node js.Value) bool
{
if node.Get("nodeType").Int() != 1 {
return false
}
return node.Call("hasAttribute", "data-router-outlet").Bool()
}
patchAttributes
func patchAttributes(oldNode, newNode js.Value)
{
oldAttrs := oldNode.Call("getAttributeNames")
for i := 0; i < oldAttrs.Length(); i++ {
name := oldAttrs.Index(i).String()
if !newNode.Call("hasAttribute", name).Bool() {
oldNode.Call("removeAttribute", name)
}
}
newAttrs := newNode.Call("getAttributeNames")
for i := 0; i < newAttrs.Length(); i++ {
name := newAttrs.Index(i).String()
val := newNode.Call("getAttribute", name)
if oldNode.Call("getAttribute", name).String() != val.String() {
oldNode.Call("setAttribute", name, val)
}
}
}
TestUpdateDOMSkipsNonElementNodes
Ensure UpdateDOM handles nodes without attributes (e.g. comments) without panicking.
Parameters
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-->")
}
Event
Event wraps a browser event.
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") }
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
Returns
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
})
}
ScheduleRender
ScheduleRender updates the DOM of the specified component after a delay.
Parameters
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()
})
}
binding
binding represents a precompiled event binding.
type binding struct
Fields
| Name | Type | Description |
|---|---|---|
| Path | []int | |
| Event | string | |
| Handler | string | |
| Modifiers | []string |
RegisterBindings
RegisterBindings generates and associates bindings for a component instance.
Parameters
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
}
OverrideBindings
OverrideBindings replaces the cached bindings for a component name.
Parameters
func OverrideBindings(name, template string)
{
bs, err := parseTemplate(template)
if err != nil {
return
}
precompiledByName[name] = bs
}
parseTemplate
Parameters
Returns
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
}
collectBindings
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
}
replaceEventHandlers
Parameters
Returns
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
})
}
TestDocumentElementBasics
Parameters
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)
}
}
TestDocumentHead
Parameters
func TestDocumentHead(t *testing.T)
{
doc := Doc()
if node := doc.Head().Get("nodeName").String(); node != "HEAD" {
t.Fatalf("Head() node = %q", node)
}
}
TestPlaceholder
Parameters
func TestPlaceholder(_ *testing.T)
{}
TestElementRemoveAttr
Parameters
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")
}
}
TestElementMatches
Parameters
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")
}
}
TestMissingElementHelpersAreSafe
Parameters
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")
}
}
TestFromWrapsRawValue
Parameters
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")
}
}
CreateElement
CreateElement returns a new element with the given tag name.
Parameters
Returns
func CreateElement(tag string) Element
{ return Doc().CreateElement(tag) }
Uses
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.
func From(v js.Value) Element
{ return Element{v} }
Uses
ByID
ByID fetches an element by its id attribute.
Parameters
Returns
func ByID(id string) Element
{ return Doc().ByID(id) }
Uses
Query
Query returns the first element matching the CSS selector.
Parameters
Returns
func Query(selector string) Element
{ return Doc().Query(selector) }
Uses
QueryAll
QueryAll returns all elements matching the CSS selector.
Parameters
Returns
func QueryAll(selector string) Element
{ return Doc().QueryAll(selector) }
Uses
ByClass
ByClass returns all elements with the given class name.
Parameters
Returns
func ByClass(name string) Element
{ return Doc().ByClass(name) }
Uses
ByTag
ByTag returns all elements with the given tag name.
Parameters
Returns
func ByTag(tag string) Element
{ return Doc().ByTag(tag) }
Uses
SetInnerHTML
SetInnerHTML replaces an element’s children with the provided HTML string.
Parameters
func SetInnerHTML(el Element, html string)
{ el.SetHTML(html) }
Uses
Text
Text returns an element’s text content.
Parameters
Returns
func Text(el Element) string
{ return el.Text() }
Uses
SetText
SetText sets an element’s text content.
Parameters
func SetText(el Element, text string)
{ el.SetText(text) }
Uses
Attr
Attr retrieves the value of an attribute or an empty string if unset.
Parameters
Returns
func Attr(el Element, name string) string
{ return el.Attr(name) }
Uses
SetAttr
SetAttr sets the value of an attribute on the element.
Parameters
func SetAttr(el Element, name, value string)
{ el.SetAttr(name, value) }
Uses
AddClass
AddClass adds a class to the element’s class list.
Parameters
func AddClass(el Element, class string)
{ el.AddClass(class) }
Uses
RemoveClass
RemoveClass removes a class from the element’s class list.
Parameters
func RemoveClass(el Element, class string)
{ el.RemoveClass(class) }
Uses
HasClass
HasClass reports whether the element has the specified class.
Parameters
Returns
func HasClass(el Element, class string) bool
{ return el.HasClass(class) }
Uses
ToggleClass
ToggleClass toggles the presence of a class on the element’s class list.
Parameters
func ToggleClass(el Element, class string)
{ el.ToggleClass(class) }
Uses
SetStyle
SetStyle sets an inline style property on the element.
Parameters
func SetStyle(el Element, prop, value string)
{ el.SetStyle(prop, value) }
Uses
eventOptions
Returns
func eventOptions() js.Dict
{
opts := js.NewDict()
opts.Set("bubbles", true)
opts.Set("cancelable", true)
return opts
}
mountHandlerRoot
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
}
Uses
TestComponentHandlersAreScoped
Parameters
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)
}
}
TestDelegatedEventModifiers
Parameters
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)
}
}
TestDelegatedKeyAndTimingModifiers
Parameters
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)
}
}
TestDelegatedFocusHandlerUsesCaptureListener
Parameters
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)
}
}
LifecycleHook
LifecycleHook observes a component root after mount, update, and unmount.
type LifecycleHook struct
Fields
| Name | Type | Description |
|---|---|---|
| Mounted | func(Element) func() | |
| Updated | func(Element) | |
| Unmounted | func(Element) |
lifecycleRecord
type lifecycleRecord struct
Methods
Parameters
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()
}
Parameters
Returns
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 |
Uses
componentLifecycle
type componentLifecycle struct
Fields
| Name | Type | Description |
|---|---|---|
| mounted | bool | |
| hooks | []*lifecycleRecord |
RegisterLifecycleHook
RegisterLifecycleHook registers a hook and returns a cancellation function.
Parameters
Returns
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()
}
})
}
}
Uses
MountLifecycleHooks
MountLifecycleHooks activates every hook registered for a component.
Parameters
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))
}
}
}
UpdateLifecycleHooks
UpdateLifecycleHooks notifies mounted component hooks after a DOM patch.
Parameters
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)
})
}
}
}
UnmountLifecycleHooks
UnmountLifecycleHooks runs hook cleanup while the component root still exists.
Parameters
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)
})
}
}
}
runMountedHook
Parameters
Returns
func runMountedHook(componentID string, hook func(Element) func(), root Element) (cleanup func())
{
runLifecycleHook(componentID, "mounted", func() {
cleanup = hook(root)
})
return cleanup
}
Uses
runLifecycleHook
Parameters
func runLifecycleHook(componentID, phase string, fn func())
{
defer func() {
if recovered := recover(); recovered != nil && OnHandlerPanic != nil {
OnHandlerPanic(recovered, "DOM "+phase+": "+componentID)
}
}()
fn()
}
ownComponentRoot
Parameters
Returns
func ownComponentRoot(componentID string) Element
{
root := ComponentRoot(componentID)
if root.IsNull() || root.IsUndefined() || root.Attr("data-component-id") != componentID {
return Element{}
}
return root
}
Uses
TestExpandEvents
Parameters
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)
}
}
}