//go:build js && wasm // Package wasmruntime is the wasm client runtime for the neutral vdom package: it // reconciles vdom.VNode trees into the real DOM (fresh mount or hydration of // server-rendered DOM), and drives re-renders when signals change. package wasmruntime import ( "syscall/js" "kjol/vdom" ) var document = js.Global().Get("document") // nodeRT is the per-node bookkeeping stored in VNode.Runtime (wasm only). type nodeRT struct { dom js.Value jsFuncs map[string]js.Func refs map[string]*handlerRef } type handlerRef struct{ fn func(vdom.Event) } func rt(n *vdom.VNode) *nodeRT { if n.Runtime == nil { n.Runtime = &nodeRT{jsFuncs: map[string]js.Func{}, refs: map[string]*handlerRef{}} } return n.Runtime.(*nodeRT) } // clientEvent adapts a DOM event to vdom.Event. type clientEvent struct{ js js.Value } func (e clientEvent) PreventDefault() { e.js.Call("preventDefault") } func (e clientEvent) Value() string { t := e.js.Get("target") if !t.Truthy() { return "" } v := t.Get("value") if !v.Truthy() { return "" // e.g. a button has no value } return v.String() } func one(n *vdom.VNode) []*vdom.VNode { if n == nil { return nil } return []*vdom.VNode{n} } // ---- fresh create + diff ---- func createDOM(n *vdom.VNode) js.Value { if n.Tag == "" { d := document.Call("createTextNode", n.Text) rt(n).dom = d return d } el := document.Call("createElement", n.Tag) rt(n).dom = el for k, v := range n.Attrs { el.Call("setAttribute", k, v) } for k, v := range n.Props { el.Set(k, v) } for name, h := range n.Events { addListener(n, name, h) } if n.HTML != "" { el.Set("innerHTML", n.HTML) return el } for _, c := range n.Children { el.Call("appendChild", createDOM(c)) } return el } func patchChildren(parent js.Value, old, next []*vdom.VNode) { n := max(len(old), len(next)) for i := range n { var o, x *vdom.VNode if i < len(old) { o = old[i] } if i < len(next) { x = next[i] } patch(parent, o, x) } } func patch(parent js.Value, o, x *vdom.VNode) { switch { case o == nil && x == nil: return case o == nil: parent.Call("appendChild", createDOM(x)) case x == nil: parent.Call("removeChild", rt(o).dom) release(o) case o.Tag != x.Tag: parent.Call("replaceChild", createDOM(x), rt(o).dom) release(o) default: x.Runtime = o.Runtime // adopt dom + listeners dom := rt(x).dom if x.Tag == "" { if x.Text != o.Text { dom.Set("nodeValue", x.Text) } return } updateAttrs(o, x) updateProps(o, x) updateEvents(o, x) if x.HTML != "" { if x.HTML != o.HTML { dom.Set("innerHTML", x.HTML) } return } patchChildren(dom, o.Children, x.Children) } } func updateAttrs(o, x *vdom.VNode) { dom := rt(x).dom for k := range o.Attrs { if _, ok := x.Attrs[k]; !ok { dom.Call("removeAttribute", k) } } for k, v := range x.Attrs { if o.Attrs[k] != v { dom.Call("setAttribute", k, v) } } } func updateProps(o, x *vdom.VNode) { dom := rt(x).dom for k, v := range x.Props { if o.Props[k] != v && dom.Get(k).String() != v { dom.Set(k, v) } } } func updateEvents(o, x *vdom.VNode) { r := rt(x) // same nodeRT as o (adopted above) for name, fn := range r.jsFuncs { if _, ok := x.Events[name]; !ok { r.dom.Call("removeEventListener", name, fn) fn.Release() delete(r.jsFuncs, name) delete(r.refs, name) } } for name, h := range x.Events { if ref, ok := r.refs[name]; ok { ref.fn = h } else { addListener(x, name, h) } } } func addListener(n *vdom.VNode, name string, handler func(vdom.Event)) { r := rt(n) ref := &handlerRef{fn: handler} fn := js.FuncOf(func(this js.Value, args []js.Value) any { var ev js.Value if len(args) > 0 { ev = args[0] } ref.fn(clientEvent{js: ev}) return nil }) r.dom.Call("addEventListener", name, fn) r.jsFuncs[name] = fn r.refs[name] = ref } func release(n *vdom.VNode) { if n.Runtime != nil { for _, fn := range rt(n).jsFuncs { fn.Release() } } for _, c := range n.Children { release(c) } } // ---- hydration: adopt server-rendered DOM instead of creating it ---- func hydrateNode(dom js.Value, n *vdom.VNode) { if !dom.Truthy() { return // structural mismatch; leave a hole (a later re-render will fix) } rt(n).dom = dom if n.Tag == "" { if dom.Get("nodeValue").String() != n.Text { dom.Set("nodeValue", n.Text) } return } for name, h := range n.Events { addListener(n, name, h) } for k, v := range n.Props { dom.Set(k, v) } if n.HTML != "" { return // trust server-rendered HTML } childNodes := dom.Get("childNodes") for i, c := range n.Children { hydrateNode(childNodes.Index(i), c) } }