add WASM blazor-like thing
This commit is contained in:
17
go/vdom/events.go
Normal file
17
go/vdom/events.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package vdom
|
||||
|
||||
// DOM event-name constants for On / OnEvent.
|
||||
const (
|
||||
EVENT_CLICK = "click"
|
||||
EVENT_DBLCLICK = "dblclick"
|
||||
EVENT_INPUT = "input"
|
||||
EVENT_CHANGE = "change"
|
||||
EVENT_SUBMIT = "submit"
|
||||
EVENT_KEYDOWN = "keydown"
|
||||
EVENT_KEYUP = "keyup"
|
||||
EVENT_FOCUS = "focus"
|
||||
EVENT_BLUR = "blur"
|
||||
EVENT_MOUSEDOWN = "mousedown"
|
||||
EVENT_MOUSEUP = "mouseup"
|
||||
EVENT_MOUSEMOVE = "mousemove"
|
||||
)
|
||||
7
go/vdom/mode_native.go
Normal file
7
go/vdom/mode_native.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !(js && wasm)
|
||||
|
||||
package vdom
|
||||
|
||||
// IsClient is false on the server (native). Components use it to guard
|
||||
// client-only effects (e.g. fetching) so they don't run during SSR.
|
||||
const IsClient = false
|
||||
6
go/vdom/mode_wasm.go
Normal file
6
go/vdom/mode_wasm.go
Normal file
@@ -0,0 +1,6 @@
|
||||
//go:build js && wasm
|
||||
|
||||
package vdom
|
||||
|
||||
// IsClient is true in the browser (wasm).
|
||||
const IsClient = true
|
||||
81
go/vdom/signal.go
Normal file
81
go/vdom/signal.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package vdom
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
// Schedule is installed by the wasm runtime at startup; a signal write
|
||||
// triggers a re-render. On the server it is nil (renders are one-shot).
|
||||
var Schedule func()
|
||||
|
||||
// Signal holds state. On the client, writing it schedules a re-render.
|
||||
type Signal[T any] struct{ v T }
|
||||
|
||||
func NewSignal[T any](initial T) *Signal[T] {
|
||||
s := &Signal[T]{v: initial}
|
||||
if active != nil {
|
||||
active.adopt(s) // server component round-trip: restore prior value, track for snapshot
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Signal[T]) Get() T { return s.v }
|
||||
|
||||
func (s *Signal[T]) Set(v T) {
|
||||
s.v = v
|
||||
if Schedule != nil {
|
||||
Schedule()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Signal[T]) Update(fn func(T) T) { s.Set(fn(s.v)) }
|
||||
|
||||
func (s *Signal[T]) snapshot() []byte { b, _ := json.Marshal(s.v); return b }
|
||||
func (s *Signal[T]) restoreFrom(b []byte) { _ = json.Unmarshal(b, &s.v) }
|
||||
|
||||
// ---- signal-state round-trip for server components ----
|
||||
//
|
||||
// A server component is stateless on the server: its signal values are
|
||||
// serialized and round-tripped through the client. While a Collector is active
|
||||
// (during a server-component render on the server), NewSignal restores each
|
||||
// signal's value from the incoming snapshot by creation order (hook-order), and
|
||||
// remembers it so the new values can be snapshotted back out.
|
||||
|
||||
type signalState interface {
|
||||
snapshot() []byte
|
||||
restoreFrom([]byte)
|
||||
}
|
||||
|
||||
// Collector captures the signals created during a server render.
|
||||
type Collector struct {
|
||||
restore [][]byte
|
||||
idx int
|
||||
sigs []signalState
|
||||
}
|
||||
|
||||
var active *Collector
|
||||
|
||||
func (c *Collector) adopt(s signalState) {
|
||||
if c.idx < len(c.restore) {
|
||||
s.restoreFrom(c.restore[c.idx])
|
||||
}
|
||||
c.idx++
|
||||
c.sigs = append(c.sigs, s)
|
||||
}
|
||||
|
||||
// BeginCollect starts collecting signals, restoring them from `restore` (which
|
||||
// may be nil for an initial mount). Call EndCollect when construction is done.
|
||||
func BeginCollect(restore [][]byte) *Collector {
|
||||
active = &Collector{restore: restore}
|
||||
return active
|
||||
}
|
||||
|
||||
// EndCollect stops collecting (rendering/handlers may still read the signals).
|
||||
func EndCollect() { active = nil }
|
||||
|
||||
// Snapshot returns the current values of the collected signals, in order.
|
||||
func (c *Collector) Snapshot() [][]byte {
|
||||
out := make([][]byte, len(c.sigs))
|
||||
for i, s := range c.sigs {
|
||||
out[i] = s.snapshot()
|
||||
}
|
||||
return out
|
||||
}
|
||||
30
go/vdom/tags.go
Normal file
30
go/vdom/tags.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package vdom
|
||||
|
||||
// HTML tag helpers over El, for dot-import.
|
||||
func Div(m ...Mod) *VNode { return El("div", m...) }
|
||||
func Span(m ...Mod) *VNode { return El("span", m...) }
|
||||
func P(m ...Mod) *VNode { return El("p", m...) }
|
||||
func Section(m ...Mod) *VNode { return El("section", m...) }
|
||||
func Nav(m ...Mod) *VNode { return El("nav", m...) }
|
||||
func Header(m ...Mod) *VNode { return El("header", m...) }
|
||||
func Main(m ...Mod) *VNode { return El("main", m...) }
|
||||
func Footer(m ...Mod) *VNode { return El("footer", m...) }
|
||||
func H1(m ...Mod) *VNode { return El("h1", m...) }
|
||||
func H2(m ...Mod) *VNode { return El("h2", m...) }
|
||||
func H3(m ...Mod) *VNode { return El("h3", m...) }
|
||||
func Hr(m ...Mod) *VNode { return El("hr", m...) }
|
||||
func A(m ...Mod) *VNode { return El("a", m...) }
|
||||
func Strong(m ...Mod) *VNode { return El("strong", m...) }
|
||||
func Em(m ...Mod) *VNode { return El("em", m...) }
|
||||
func Small(m ...Mod) *VNode { return El("small", m...) }
|
||||
func Code(m ...Mod) *VNode { return El("code", m...) }
|
||||
func Label(m ...Mod) *VNode { return El("label", m...) }
|
||||
func Ul(m ...Mod) *VNode { return El("ul", m...) }
|
||||
func Li(m ...Mod) *VNode { return El("li", m...) }
|
||||
func Form(m ...Mod) *VNode { return El("form", m...) }
|
||||
func Input(m ...Mod) *VNode { return El("input", m...) }
|
||||
func Button(m ...Mod) *VNode { return El("button", m...) }
|
||||
func Table(m ...Mod) *VNode { return El("table", m...) }
|
||||
func Tr(m ...Mod) *VNode { return El("tr", m...) }
|
||||
func Td(m ...Mod) *VNode { return El("td", m...) }
|
||||
func Img(m ...Mod) *VNode { return El("img", m...) }
|
||||
138
go/vdom/vnode.go
Normal file
138
go/vdom/vnode.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Package vdom is a platform-neutral virtual DOM shared by the server (renders
|
||||
// to an HTML string) and the client (reconciles into the real DOM). It compiles
|
||||
// on BOTH native and js/wasm, so the same component code runs in both places —
|
||||
// which is what makes server-side rendering + client hydration possible.
|
||||
package vdom
|
||||
|
||||
import (
|
||||
"html"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Event is a DOM event passed to handlers. The client provides a concrete
|
||||
// implementation; on the server events are never invoked.
|
||||
type Event interface {
|
||||
PreventDefault()
|
||||
Value() string // target.value (for inputs)
|
||||
}
|
||||
|
||||
// VNode is a virtual DOM node. Tag == "" is a text node (content in Text). HTML,
|
||||
// if set on an element, is raw innerHTML (children ignored).
|
||||
type VNode struct {
|
||||
Tag string
|
||||
Text string
|
||||
HTML string
|
||||
Attrs map[string]string
|
||||
Props map[string]string
|
||||
Events map[string]func(Event)
|
||||
Children []*VNode
|
||||
|
||||
// Runtime holds the wasm reconciler's per-node bookkeeping (DOM handle,
|
||||
// listener wrappers). It's `any` so this package stays platform-neutral; it
|
||||
// is nil on the server.
|
||||
Runtime any
|
||||
}
|
||||
|
||||
// Mod configures a VNode while it is built.
|
||||
type Mod interface{ apply(*VNode) }
|
||||
|
||||
// El builds an element VNode.
|
||||
func El(tag string, mods ...Mod) *VNode {
|
||||
n := &VNode{Tag: tag, Attrs: map[string]string{}, Props: map[string]string{}, Events: map[string]func(Event){}}
|
||||
for _, m := range mods {
|
||||
m.apply(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// Text builds a text VNode.
|
||||
func Text(s string) *VNode { return &VNode{Text: s} }
|
||||
|
||||
func (n *VNode) apply(parent *VNode) { parent.Children = append(parent.Children, n) }
|
||||
|
||||
type attrMod struct{ k, v string }
|
||||
|
||||
func (a attrMod) apply(n *VNode) { n.Attrs[a.k] = a.v }
|
||||
|
||||
// Attr sets an HTML attribute.
|
||||
func Attr(k, v string) Mod { return attrMod{k, v} }
|
||||
|
||||
type propMod struct{ k, v string }
|
||||
|
||||
func (p propMod) apply(n *VNode) { n.Props[p.k] = p.v }
|
||||
|
||||
// Prop sets a live DOM property (e.g. an input's value).
|
||||
func Prop(k, v string) Mod { return propMod{k, v} }
|
||||
|
||||
type htmlMod struct{ html string }
|
||||
|
||||
func (h htmlMod) apply(n *VNode) { n.HTML = h.html }
|
||||
|
||||
// Raw sets inner HTML verbatim (children ignored) — e.g. a server-computed SVG.
|
||||
func Raw(html string) Mod { return htmlMod{html} }
|
||||
|
||||
type eventMod struct {
|
||||
name string
|
||||
h func(Event)
|
||||
}
|
||||
|
||||
func (e eventMod) apply(n *VNode) { n.Events[e.name] = e.h }
|
||||
|
||||
// On registers an event handler that ignores the event object.
|
||||
func On(event string, h func()) Mod { return eventMod{event, func(Event) { h() }} }
|
||||
|
||||
// OnEvent registers an event handler that receives the Event.
|
||||
func OnEvent(event string, h func(Event)) Mod { return eventMod{event, h} }
|
||||
|
||||
// --- server-side HTML rendering (used for SSR; runs on any platform) ---
|
||||
|
||||
var voidTags = map[string]bool{"br": true, "hr": true, "img": true, "input": true, "meta": true, "link": true}
|
||||
|
||||
// RenderHTML serializes a VNode tree to HTML with NO extra whitespace, so the
|
||||
// browser's parsed childNodes line up 1:1 with the VNode children on hydration.
|
||||
func RenderHTML(n *VNode) string {
|
||||
var b strings.Builder
|
||||
writeNode(&b, n)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func writeNode(b *strings.Builder, n *VNode) {
|
||||
if n.Tag == "" {
|
||||
b.WriteString(html.EscapeString(n.Text))
|
||||
return
|
||||
}
|
||||
b.WriteByte('<')
|
||||
b.WriteString(n.Tag)
|
||||
writeAttrs(b, n.Attrs)
|
||||
writeAttrs(b, n.Props) // props like input value show up as attributes in SSR
|
||||
b.WriteByte('>')
|
||||
if voidTags[n.Tag] {
|
||||
return
|
||||
}
|
||||
if n.HTML != "" {
|
||||
b.WriteString(n.HTML) // raw
|
||||
} else {
|
||||
for _, c := range n.Children {
|
||||
writeNode(b, c)
|
||||
}
|
||||
}
|
||||
b.WriteString("</")
|
||||
b.WriteString(n.Tag)
|
||||
b.WriteByte('>')
|
||||
}
|
||||
|
||||
func writeAttrs(b *strings.Builder, m map[string]string) {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
for _, k := range keys {
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(k)
|
||||
b.WriteString(`="`)
|
||||
b.WriteString(html.EscapeString(m[k]))
|
||||
b.WriteByte('"')
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user