add WASM blazor-like thing
This commit is contained in:
228
go/wasmruntime/reconcile.go
Normal file
228
go/wasmruntime/reconcile.go
Normal file
@@ -0,0 +1,228 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user