Files
kjol/go/cmd/examples/go-wasm-web/app/pages.go

194 lines
7.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package app holds the application's pages and components as standalone
// functions (no central App struct). It is platform-neutral, so the SAME code
// renders on the server (SSR) and hydrates on the client.
//
// Directives (processed by cmd/gowasmgen at build time):
//
// //gowasm:page <path> [static] [layout=<name>]
// marks a page factory as a route. `static`
// pre-renders it on the server (SSR);
// `layout=<name>` wraps it in a //gowasm:layout.
// //gowasm:layout <name> marks a func(Deps, *VNode) *VNode as a named
// layout that wraps a page's content.
// //gowasm:server (see server_counter.go) marks a component
// that runs on the server; the generated
// client stub makes calling it identical to
// calling any other component.
//
// The Routes() map, StaticPaths set, and RouteLayout/LayoutFor dispatch are all
// generated from these directives.
package app
//go:generate go run kjol/cmd/gowasmgen .
import (
"strconv"
. "kjol/vdom"
)
// Deps are the client-only capabilities, injected so the pages stay neutral.
type Deps struct {
Path func() string
Navigate func(string)
}
func itoa(n int) string { return strconv.Itoa(n) }
// Layout wraps a page's rendered content with shared chrome (nav, footer, …).
// Layouts are declared with //gowasm:layout and selected per route via a page's
// `layout=` directive; the generated LayoutFor dispatches by name.
type Layout func(d Deps, content *VNode) *VNode
// Shell renders the current route's page inside its declared layout. `routes` is
// the generated route table; `d.Path()` selects both the page and its layout.
func Shell(d Deps, routes map[string]func() *VNode) *VNode {
path := d.Path()
var content *VNode
if page := routes[path]; page != nil {
content = page()
} else {
content = notFound(path)
}
return LayoutFor(d, path, content)
}
func notFound(path string) *VNode {
return Div(
H2(Attr("class", "h4 mb-3"), Text("Page not found")),
P(Attr("class", "text-secondary"), Text("No route matches "+path+".")),
)
}
// --- layouts (selected per route via `layout=` in //gowasm:page) ---------
// PublicLayout is the chrome for public/marketing pages: a light navbar with a
// call-to-action into the app, and a footer. The func(Deps, *VNode) *VNode shape
// is what //gowasm:layout expects.
//
//gowasm:layout public
func PublicLayout(d Deps, content *VNode) *VNode {
return Div(
Nav(Attr("class", "navbar navbar-expand bg-light border-bottom mb-4"),
Div(Attr("class", "container"),
A(Attr("class", "navbar-brand fw-bold"), Attr("href", "/"), navigate(d, "/"), Text("gowasm")),
Ul(Attr("class", "navbar-nav ms-auto align-items-center"),
navItem(d, "/", "Home"),
navItem(d, "/about", "About"),
Li(Attr("class", "nav-item ms-2"),
A(Attr("class", "btn btn-sm btn-primary"), Attr("href", "/chart"),
navigate(d, "/chart"), Text("Open app →"))),
))),
Main(Attr("class", "container"),
content,
Footer(Attr("class", "text-secondary small border-top mt-5 pt-3"),
Text("gowasm public site — a tiny Blazor-like engine in Go.")),
),
)
}
// AppLayout is the chrome for the application itself: a dark app navbar listing
// the app's sections, plus a link back to the public site.
//
//gowasm:layout app
func AppLayout(d Deps, content *VNode) *VNode {
return Div(
Nav(Attr("class", "navbar navbar-expand navbar-dark bg-dark mb-4"),
Div(Attr("class", "container"),
A(Attr("class", "navbar-brand fw-bold"), Attr("href", "/chart"), navigate(d, "/chart"), Text("gowasm · app")),
Ul(Attr("class", "navbar-nav me-auto"),
navItem(d, "/chart", "Chart"),
navItem(d, "/server", "Server")),
Ul(Attr("class", "navbar-nav"),
navItem(d, "/", "Home")),
)),
Main(Attr("class", "container"), content),
)
}
// navItem is a nav link that carries an active state on the current route.
func navItem(d Deps, path, label string) *VNode {
cls := "nav-link"
if d.Path() == path {
cls += " active"
}
return Li(Attr("class", "nav-item"),
A(Attr("class", cls), Attr("href", path), navigate(d, path), Text(label)))
}
// navigate intercepts a link click for client-side SPA navigation. On the server
// Navigate is nil, so the anchor falls back to a normal navigation.
func navigate(d Deps, path string) Mod {
return OnEvent(EVENT_CLICK, func(e Event) {
if d.Navigate != nil {
e.PreventDefault()
d.Navigate(path)
}
})
}
// Counter is a presentational client component; state is owned by the caller.
func Counter(label string, count *Signal[int]) *VNode {
return Div(Attr("class", "counter card mb-2"),
Div(Attr("class", "card-body py-2 d-flex align-items-center"),
Span(Attr("class", "me-2 fw-semibold"), Text(label+": ")),
Strong(Attr("class", "badge text-bg-primary me-2"), Text(itoa(count.Get()))),
Div(Attr("class", "btn-group btn-group-sm ms-auto"),
Button(Attr("class", "btn btn-outline-secondary"),
On(EVENT_CLICK, func() { count.Update(func(v int) int { return v - 1 }) }), Text("")),
Button(Attr("class", "btn btn-outline-primary"),
On(EVENT_CLICK, func() { count.Update(func(v int) int { return v + 1 }) }), Text("+")),
)))
}
//gowasm:page / static layout=public
func HomePage(d Deps) func() *VNode {
a := NewSignal(0)
b := NewSignal(0)
return func() *VNode {
return Div(
H2(Attr("class", "h4 mb-3"), Text("Home — component composition")),
P(Attr("class", "text-secondary"), Text("Two counters; the total is derived across them. Server-rendered, then hydrated.")),
Counter("Apples", a),
Counter("Bananas", b),
Div(Attr("class", "alert alert-info d-flex justify-content-between align-items-center mt-3"),
Span(Text("Combined total: ")),
Strong(Attr("class", "fs-5"), Text(itoa(a.Get()+b.Get())))),
)
}
}
//gowasm:page /about static layout=public
func AboutPage(d Deps) func() *VNode {
return func() *VNode {
return Div(
H2(Attr("class", "h4 mb-3"), Text("About")),
P(Attr("class", "lead"),
Text("Components are standalone functions; calling a server component looks "+
"identical to calling a client one — the //gowasm:server directive and the "+
"build-time codegen wire up the round-trip. Static routes are SSR'd; the rest "+
"render on the client.")),
)
}
}
//gowasm:page /server layout=app
func ServerPage(d Deps) func() *VNode {
// ServerCounter is a server component — but calling it is just like calling
// any component. On the client this resolves to a generated stub that mounts
// it over /rsc; on the server it's the real function. (Not `static`: the
// client renders the stub, which round-trips, so there's nothing stable to
// pre-render + hydrate.)
counter := ServerCounter()
return func() *VNode {
return Div(
H2(Attr("class", "h4 mb-3"), Text("Server component")),
P(Attr("class", "text-secondary"),
Text("This counter runs on the server. Its state lives there; clicks round-trip "+
"and the returned render merges into the DOM. The call site is identical to a "+
"client component.")),
counter(),
)
}
}