94 lines
2.7 KiB
Go
94 lines
2.7 KiB
Go
//go:build js && wasm
|
|
|
|
package rsc
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/gob"
|
|
"syscall/js"
|
|
|
|
"kjol/vdom"
|
|
)
|
|
|
|
// Mount connects to a server component by name and returns a render function.
|
|
// The generated client stub for a //gowasm:server component just calls this, so
|
|
// the call site is identical to a client component. It POSTs {name, state,
|
|
// event} to /rsc and swaps (reconciles) the returned render into the DOM. State
|
|
// is opaque and round-trips through the client — the server keeps nothing.
|
|
func Mount(name string) func() *vdom.VNode {
|
|
tree := vdom.NewSignal[*vdom.VNode](nil)
|
|
var state [][]byte
|
|
var post func(kind string, nodeID int, event, value string)
|
|
|
|
var convert func(*SNode) *vdom.VNode
|
|
convert = func(sn *SNode) *vdom.VNode {
|
|
if sn == nil {
|
|
return nil
|
|
}
|
|
if sn.Tag == "" {
|
|
return vdom.Text(sn.Text)
|
|
}
|
|
mods := make([]vdom.Mod, 0, len(sn.Attrs)+len(sn.Events)+len(sn.Kids)+1)
|
|
for k, v := range sn.Attrs {
|
|
mods = append(mods, vdom.Attr(k, v))
|
|
}
|
|
if sn.HTML != "" {
|
|
mods = append(mods, vdom.Raw(sn.HTML))
|
|
}
|
|
for _, ev := range sn.Events {
|
|
event, id := ev, sn.ID
|
|
mods = append(mods, vdom.OnEvent(ev, func(e vdom.Event) { post("event", id, event, e.Value()) }))
|
|
}
|
|
for _, k := range sn.Kids {
|
|
if c := convert(k); c != nil {
|
|
mods = append(mods, c)
|
|
}
|
|
}
|
|
return vdom.El(sn.Tag, mods...)
|
|
}
|
|
|
|
onArr := js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
arr := js.Global().Get("Uint8Array").New(args[0])
|
|
b := make([]byte, arr.Get("length").Int())
|
|
js.CopyBytesToGo(b, arr)
|
|
var resp Response
|
|
if gob.NewDecoder(bytes.NewReader(b)).Decode(&resp) == nil {
|
|
state = resp.State
|
|
tree.Set(convert(resp.Tree)) // re-render -> reconcile into the DOM
|
|
}
|
|
return nil
|
|
})
|
|
onResp := js.FuncOf(func(this js.Value, args []js.Value) any {
|
|
args[0].Call("arrayBuffer").Call("then", onArr)
|
|
return nil
|
|
})
|
|
|
|
loc := js.Global().Get("location")
|
|
url := loc.Get("protocol").String() + "//" + loc.Get("host").String() + "/rsc"
|
|
|
|
post = func(kind string, nodeID int, event, value string) {
|
|
var buf bytes.Buffer
|
|
if gob.NewEncoder(&buf).Encode(Request{Kind: kind, Name: name, State: state, NodeID: nodeID, Event: event, Value: value}) != nil {
|
|
return
|
|
}
|
|
body := js.Global().Get("Uint8Array").New(buf.Len())
|
|
js.CopyBytesToJS(body, buf.Bytes())
|
|
opts := js.Global().Get("Object").New()
|
|
opts.Set("method", "POST")
|
|
opts.Set("body", body)
|
|
js.Global().Call("fetch", url, opts).Call("then", onResp)
|
|
}
|
|
|
|
started := false
|
|
return func() *vdom.VNode {
|
|
if !started {
|
|
started = true
|
|
post("mount", 0, "", "")
|
|
}
|
|
if t := tree.Get(); t != nil {
|
|
return t
|
|
}
|
|
return vdom.Div(vdom.Attr("class", "text-muted"), vdom.Text("loading server component…"))
|
|
}
|
|
}
|