feat: add working prototype for via
This commit is contained in:
224
context.go
Normal file
224
context.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package via
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-via/via/h"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
|
||||
// Context is the living bridge between Go and the browser.
|
||||
//
|
||||
// It binds user state and actions, manages reactive signals, and defines UI through View.
|
||||
type Context struct {
|
||||
id string
|
||||
route string
|
||||
app *via
|
||||
view func() h.H
|
||||
sse *datastar.ServerSentEventGenerator
|
||||
actionRegistry map[string]func()
|
||||
signals map[string]*signal
|
||||
signalsMux sync.Mutex
|
||||
createdAt time.Time
|
||||
}
|
||||
|
||||
// View defines the UI rendered by this context.
|
||||
// The function should return an h.H element (from via/h).
|
||||
//
|
||||
// Changes to signals or state can be pushed live with Sync().
|
||||
func (c *Context) View(f func() h.H) {
|
||||
if f == nil {
|
||||
c.app.logErr(c, "failed to bind view to context: nil func")
|
||||
}
|
||||
c.view = func() h.H { return h.Div(h.ID(c.id), f()) }
|
||||
}
|
||||
|
||||
type actionTrigger struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (a *actionTrigger) OnClick() h.H {
|
||||
return h.Data("on:click", fmt.Sprintf("@get('/_action/%s')", a.id))
|
||||
}
|
||||
|
||||
// Action registers a named event handler callable from the browser.
|
||||
//
|
||||
// Use h.OnClick("actionName") or similar event bindings to trigger actions.
|
||||
// Signal updates from the browser are automatically injected in the context before the
|
||||
// handler function executes.
|
||||
func (c *Context) Action(f func()) *actionTrigger {
|
||||
// if id == "" {
|
||||
// c.app.logErr(c, "failed to bind action to context: id is ''")
|
||||
// }
|
||||
id := genRandID()
|
||||
if f == nil {
|
||||
c.app.logErr(c, "failed to bind action '%s' to context: nil func", id)
|
||||
return nil
|
||||
}
|
||||
c.actionRegistry[id] = f
|
||||
return &actionTrigger{id}
|
||||
}
|
||||
|
||||
func (c *Context) getActionFn(id string) (func(), error) {
|
||||
if f, ok := c.actionRegistry[id]; ok {
|
||||
return f, nil
|
||||
}
|
||||
return nil, fmt.Errorf("action '%s' not found", id)
|
||||
}
|
||||
|
||||
func (c *Context) Signals() map[string]*signal {
|
||||
if c.signals == nil {
|
||||
c.app.logErr(c, "failed to get signal: nil signals in ctx")
|
||||
return make(map[string]*signal)
|
||||
}
|
||||
return c.signals
|
||||
}
|
||||
|
||||
// Signal creates a reactive signal and initializes it with a value.
|
||||
// Use Bind() to link the value of input elements to the signal and Text() to
|
||||
// display the signal value and watch the UI update live as the input changes.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// h.Div(
|
||||
// h.P(h.Span(h.Text("Hello, ")), h.Span(mysignal.Text())),
|
||||
// h.Input(h.Value("World"), mysignal.Bind()),
|
||||
// )
|
||||
//
|
||||
// Signals are 'alive' only in the browser, but Via always injects their values into
|
||||
// the Context before each action call.
|
||||
// If any signal value is updated by the server the update is automatically sent to the
|
||||
// browser when using Sync() or SyncSignsls().
|
||||
func (c *Context) Signal(v any) *signal {
|
||||
sigID := genRandID()
|
||||
if v == nil {
|
||||
c.app.logErr(c, "failed to bind signal: nil signal value")
|
||||
dummy := "Error"
|
||||
return &signal{
|
||||
id: sigID,
|
||||
v: reflect.ValueOf(dummy),
|
||||
t: reflect.TypeOf(dummy),
|
||||
err: fmt.Errorf("context '%s' failed to bind signal '%s': nil signal value", c.id, sigID),
|
||||
}
|
||||
}
|
||||
sig := &signal{
|
||||
id: sigID,
|
||||
v: reflect.ValueOf(v),
|
||||
t: reflect.TypeOf(v),
|
||||
changed: true,
|
||||
}
|
||||
c.signals[sigID] = sig
|
||||
return sig
|
||||
|
||||
}
|
||||
|
||||
func (c *Context) injectSignals(sigs map[string]any) {
|
||||
if sigs == nil {
|
||||
c.app.logErr(c, "signal injection failed: nil signals in ctx")
|
||||
return
|
||||
}
|
||||
for k, v := range sigs {
|
||||
if _, ok := c.signals[k]; !ok {
|
||||
continue
|
||||
}
|
||||
c.signals[k].v = reflect.ValueOf(v)
|
||||
c.signals[k].changed = false
|
||||
}
|
||||
}
|
||||
|
||||
// Sync pushes the current view state and signal changes to the browser immediately
|
||||
// over the live SSE connection.
|
||||
func (c *Context) Sync() {
|
||||
if c.sse == nil {
|
||||
c.app.logErr(c, "sync view failed: no sse connection")
|
||||
}
|
||||
elemsPatch := bytes.NewBuffer(make([]byte, 0))
|
||||
if err := c.view().Render(elemsPatch); err != nil {
|
||||
c.app.logErr(c, "sync view failed: %v", err)
|
||||
return
|
||||
}
|
||||
_ = c.sse.PatchElements(elemsPatch.String())
|
||||
updatedSigs := make(map[string]any)
|
||||
for id, sig := range c.signals {
|
||||
if sig.err != nil {
|
||||
c.app.logWarn(c, "failed to sync signal '%s': %v", sig.id, sig.err)
|
||||
}
|
||||
if sig.changed && sig.err == nil {
|
||||
updatedSigs[id] = fmt.Sprintf("%v", sig.v)
|
||||
}
|
||||
}
|
||||
if len(updatedSigs) != 0 {
|
||||
_ = c.sse.MarshalAndPatchSignals(updatedSigs)
|
||||
}
|
||||
}
|
||||
|
||||
// SyncElements pushes an immediate html patch to the browser that merges DOM
|
||||
//
|
||||
// For the merge to occur, the top level element in the patch needs to have
|
||||
// an ID that matches the ID of an element that already sits in the view.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// If the view already contains the element:
|
||||
//
|
||||
// h.Div(
|
||||
// h.ID("my-element"),
|
||||
// h.P(h.Text("Hello from Via!"))
|
||||
// )
|
||||
//
|
||||
// Then, the merge will only occur if the ID of the top level element mattches 'my-element'.
|
||||
func (c *Context) SyncElements(elem h.H) {
|
||||
if c.sse == nil {
|
||||
c.app.logErr(c, "sync element failed: no sse connection")
|
||||
}
|
||||
if c.view == nil {
|
||||
c.app.logErr(c, "sync element failed: viewfn is nil")
|
||||
return
|
||||
}
|
||||
if elem == nil {
|
||||
c.app.logErr(c, "sync element failed: view func is nil")
|
||||
return
|
||||
}
|
||||
b := bytes.NewBuffer(make([]byte, 0))
|
||||
_ = elem.Render(b)
|
||||
c.sse.PatchElements(b.String())
|
||||
}
|
||||
|
||||
// SyncSignals pushes the current signal changes to the browser immediately
|
||||
// over the live SSE connection.
|
||||
func (c *Context) SyncSignals() {
|
||||
if c.sse == nil {
|
||||
c.app.logErr(c, "sync signals failed: sse connection not found")
|
||||
}
|
||||
updatedSigs := make(map[string]any)
|
||||
for id, sig := range c.signals {
|
||||
if sig.err != nil {
|
||||
c.app.logWarn(c, "signal out of sync'%s': %v", sig.id, sig.err)
|
||||
}
|
||||
if sig.changed && sig.err == nil {
|
||||
updatedSigs[id] = fmt.Sprintf("%v", sig.v)
|
||||
}
|
||||
}
|
||||
if len(updatedSigs) != 0 {
|
||||
_ = c.sse.MarshalAndPatchSignals(updatedSigs)
|
||||
}
|
||||
}
|
||||
|
||||
func newContext(id string, a *via) *Context {
|
||||
if a == nil {
|
||||
log.Fatalf("create context failed: app pointer is nil")
|
||||
}
|
||||
|
||||
return &Context{
|
||||
id: id,
|
||||
app: a,
|
||||
actionRegistry: make(map[string]func()),
|
||||
signals: make(map[string]*signal),
|
||||
createdAt: time.Now(),
|
||||
}
|
||||
}
|
||||
9
datastar.js
Normal file
9
datastar.js
Normal file
File diff suppressed because one or more lines are too long
14
go.mod
Normal file
14
go.mod
Normal file
@@ -0,0 +1,14 @@
|
||||
module github.com/go-via/via
|
||||
|
||||
go 1.25.3
|
||||
|
||||
require maragu.dev/gomponents v1.2.0
|
||||
|
||||
require github.com/starfederation/datastar-go v1.0.3
|
||||
|
||||
require (
|
||||
github.com/CAFxX/httpcompression v0.0.9 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
)
|
||||
43
go.sum
Normal file
43
go.sum
Normal file
@@ -0,0 +1,43 @@
|
||||
github.com/CAFxX/httpcompression v0.0.9 h1:0ue2X8dOLEpxTm8tt+OdHcgA+gbDge0OqFQWGKSqgrg=
|
||||
github.com/CAFxX/httpcompression v0.0.9/go.mod h1:XX8oPZA+4IDcfZ0A71Hz0mZsv/YJOgYygkFhizVPilM=
|
||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f h1:jopqB+UTSdJGEJT8tEqYyE29zN91fi2827oLET8tl7k=
|
||||
github.com/google/brotli/go/cbrotli v0.0.0-20230829110029-ed738e842d2f/go.mod h1:nOPhAkwVliJdNTkj3gXpljmWhjc4wCaVqbMJcPKWP4s=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ=
|
||||
github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/starfederation/datastar-go v1.0.3 h1:DnzgsJ6tDHDM6y5Nxsk0AGW/m8SyKch2vQg3P1xGTcU=
|
||||
github.com/starfederation/datastar-go v1.0.3/go.mod h1:stm83LQkhZkwa5GzzdPEN6dLuu8FVwxIv0w1DYkbD3w=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/gozstd v1.20.1 h1:xPnnnvjmaDDitMFfDxmQ4vpx0+3CdTg2o3lALvXTU/g=
|
||||
github.com/valyala/gozstd v1.20.1/go.mod h1:y5Ew47GLlP37EkTB+B4s7r6A5rdaeB7ftbl9zoYiIPQ=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maragu.dev/gomponents v1.2.0 h1:H7/N5htz1GCnhu0HB1GasluWeU2rJZOYztVEyN61iTc=
|
||||
maragu.dev/gomponents v1.2.0/go.mod h1:oEDahza2gZoXDoDHhw8jBNgH+3UR5ni7Ur648HORydM=
|
||||
32
h/attributes.go
Normal file
32
h/attributes.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package h
|
||||
|
||||
import gh "maragu.dev/gomponents/html"
|
||||
|
||||
func Href(v string) H {
|
||||
return gh.Href(v)
|
||||
}
|
||||
|
||||
func Type(v string) H {
|
||||
return gh.Type(v)
|
||||
}
|
||||
|
||||
func Src(v string) H {
|
||||
return gh.Src(v)
|
||||
}
|
||||
|
||||
func ID(v string) H {
|
||||
return gh.ID(v)
|
||||
}
|
||||
|
||||
func Value(v string) H {
|
||||
return gh.Value(v)
|
||||
}
|
||||
|
||||
func Placeholder(v string) H {
|
||||
return gh.Placeholder(v)
|
||||
}
|
||||
|
||||
// Data attributes automatically have their name prefixed with "data-".
|
||||
func Data(name, v string) H {
|
||||
return gh.Data(name, v)
|
||||
}
|
||||
9
h/datastar.go
Normal file
9
h/datastar.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package h
|
||||
|
||||
import "fmt"
|
||||
|
||||
type OnClickOpts string
|
||||
|
||||
func OnClick(actionid string, opt ...OnClickOpts) H {
|
||||
return Data("on:click", fmt.Sprintf("@get('/_action/%s')", actionid))
|
||||
}
|
||||
401
h/elements.go
Normal file
401
h/elements.go
Normal file
@@ -0,0 +1,401 @@
|
||||
package h
|
||||
|
||||
import (
|
||||
gh "maragu.dev/gomponents/html"
|
||||
)
|
||||
|
||||
func A(children ...H) H {
|
||||
return gh.A(retype(children)...)
|
||||
}
|
||||
|
||||
func Abbr(children ...H) H {
|
||||
return gh.Abbr(retype(children)...)
|
||||
}
|
||||
|
||||
func Address(children ...H) H {
|
||||
return gh.Address(retype(children)...)
|
||||
}
|
||||
|
||||
func Area(children ...H) H {
|
||||
return gh.Area(retype(children)...)
|
||||
}
|
||||
|
||||
func Article(children ...H) H {
|
||||
return gh.Article(retype(children)...)
|
||||
}
|
||||
|
||||
func Aside(children ...H) H {
|
||||
return gh.Aside(retype(children)...)
|
||||
}
|
||||
|
||||
func Audio(children ...H) H {
|
||||
return gh.Audio(retype(children)...)
|
||||
}
|
||||
|
||||
func B(children ...H) H {
|
||||
return gh.B(retype(children)...)
|
||||
}
|
||||
|
||||
func Base(children ...H) H {
|
||||
return gh.Base(retype(children)...)
|
||||
}
|
||||
|
||||
func BlockQuote(children ...H) H {
|
||||
return gh.BlockQuote(retype(children)...)
|
||||
}
|
||||
|
||||
func Body(children ...H) H {
|
||||
return gh.Body(retype(children)...)
|
||||
}
|
||||
|
||||
func Br(children ...H) H {
|
||||
return gh.Br(retype(children)...)
|
||||
}
|
||||
|
||||
func Button(children ...H) H {
|
||||
return gh.Button(retype(children)...)
|
||||
}
|
||||
|
||||
func Canvas(children ...H) H {
|
||||
return gh.Canvas(retype(children)...)
|
||||
}
|
||||
|
||||
func Caption(children ...H) H {
|
||||
return gh.Caption(retype(children)...)
|
||||
}
|
||||
|
||||
func Cite(children ...H) H {
|
||||
return gh.Cite(retype(children)...)
|
||||
}
|
||||
|
||||
func Code(children ...H) H {
|
||||
return gh.Code(retype(children)...)
|
||||
}
|
||||
|
||||
func Col(children ...H) H {
|
||||
return gh.Col(retype(children)...)
|
||||
}
|
||||
|
||||
func ColGroup(children ...H) H {
|
||||
return gh.ColGroup(retype(children)...)
|
||||
}
|
||||
|
||||
func DataList(children ...H) H {
|
||||
return gh.DataList(retype(children)...)
|
||||
}
|
||||
|
||||
func Dd(children ...H) H {
|
||||
return gh.Dd(retype(children)...)
|
||||
}
|
||||
|
||||
func Del(children ...H) H {
|
||||
return gh.Del(retype(children)...)
|
||||
}
|
||||
|
||||
func Details(children ...H) H {
|
||||
return gh.Details(retype(children)...)
|
||||
}
|
||||
|
||||
func Dfn(children ...H) H {
|
||||
return gh.Dfn(retype(children)...)
|
||||
}
|
||||
|
||||
func Dialog(children ...H) H {
|
||||
return gh.Dialog(retype(children)...)
|
||||
}
|
||||
|
||||
func Div(children ...H) H {
|
||||
return gh.Div(retype(children)...)
|
||||
}
|
||||
|
||||
func Dl(children ...H) H {
|
||||
return gh.Dl(retype(children)...)
|
||||
}
|
||||
|
||||
func Dt(children ...H) H {
|
||||
return gh.Dt(retype(children)...)
|
||||
}
|
||||
|
||||
func Em(children ...H) H {
|
||||
return gh.Em(retype(children)...)
|
||||
}
|
||||
|
||||
func Embed(children ...H) H {
|
||||
return gh.Embed(retype(children)...)
|
||||
}
|
||||
|
||||
func FieldSet(children ...H) H {
|
||||
return gh.FieldSet(retype(children)...)
|
||||
}
|
||||
|
||||
func FigCaption(children ...H) H {
|
||||
return gh.FigCaption(retype(children)...)
|
||||
}
|
||||
|
||||
func Figure(children ...H) H {
|
||||
return gh.Figure(retype(children)...)
|
||||
}
|
||||
|
||||
func Footer(children ...H) H {
|
||||
return gh.Footer(retype(children)...)
|
||||
}
|
||||
|
||||
func Form(children ...H) H {
|
||||
return gh.Form(retype(children)...)
|
||||
}
|
||||
|
||||
func H1(children ...H) H {
|
||||
return gh.H1(retype(children)...)
|
||||
}
|
||||
|
||||
func H2(children ...H) H {
|
||||
return gh.H2(retype(children)...)
|
||||
}
|
||||
|
||||
func H3(children ...H) H {
|
||||
return gh.H3(retype(children)...)
|
||||
}
|
||||
|
||||
func H4(children ...H) H {
|
||||
return gh.H4(retype(children)...)
|
||||
}
|
||||
|
||||
func H5(children ...H) H {
|
||||
return gh.H5(retype(children)...)
|
||||
}
|
||||
|
||||
func H6(children ...H) H {
|
||||
return gh.H6(retype(children)...)
|
||||
}
|
||||
|
||||
func Head(children ...H) H {
|
||||
return gh.Head(retype(children)...)
|
||||
}
|
||||
|
||||
func Header(children ...H) H {
|
||||
return gh.Header(retype(children)...)
|
||||
}
|
||||
|
||||
func Hr(children ...H) H {
|
||||
return gh.Hr(retype(children)...)
|
||||
}
|
||||
|
||||
func HTML(children ...H) H {
|
||||
return gh.HTML(retype(children)...)
|
||||
}
|
||||
|
||||
func I(children ...H) H {
|
||||
return gh.I(retype(children)...)
|
||||
}
|
||||
|
||||
func IFrame(children ...H) H {
|
||||
return gh.IFrame(retype(children)...)
|
||||
}
|
||||
|
||||
func Img(children ...H) H {
|
||||
return gh.Img(retype(children)...)
|
||||
}
|
||||
|
||||
func Input(children ...H) H {
|
||||
return gh.Input(retype(children)...)
|
||||
}
|
||||
|
||||
func Ins(children ...H) H {
|
||||
return gh.Ins(retype(children)...)
|
||||
}
|
||||
|
||||
func Kbd(children ...H) H {
|
||||
return gh.Kbd(retype(children)...)
|
||||
}
|
||||
|
||||
func Label(children ...H) H {
|
||||
return gh.Label(retype(children)...)
|
||||
}
|
||||
|
||||
func Legend(children ...H) H {
|
||||
return gh.Legend(retype(children)...)
|
||||
}
|
||||
|
||||
func Li(children ...H) H {
|
||||
return gh.Li(retype(children)...)
|
||||
}
|
||||
|
||||
func Link(children ...H) H {
|
||||
return gh.Link(retype(children)...)
|
||||
}
|
||||
|
||||
func Main(children ...H) H {
|
||||
return gh.Main(retype(children)...)
|
||||
}
|
||||
|
||||
func Mark(children ...H) H {
|
||||
return gh.Mark(retype(children)...)
|
||||
}
|
||||
|
||||
func Meta(children ...H) H {
|
||||
return gh.Meta(retype(children)...)
|
||||
}
|
||||
|
||||
func Meter(children ...H) H {
|
||||
return gh.Meter(retype(children)...)
|
||||
}
|
||||
|
||||
func Nav(children ...H) H {
|
||||
return gh.Nav(retype(children)...)
|
||||
}
|
||||
|
||||
func NoScript(children ...H) H {
|
||||
return gh.NoScript(retype(children)...)
|
||||
}
|
||||
|
||||
func Object(children ...H) H {
|
||||
return gh.Object(retype(children)...)
|
||||
}
|
||||
|
||||
func Ol(children ...H) H {
|
||||
return gh.Ol(retype(children)...)
|
||||
}
|
||||
|
||||
func OptGroup(children ...H) H {
|
||||
return gh.OptGroup(retype(children)...)
|
||||
}
|
||||
|
||||
func Option(children ...H) H {
|
||||
return gh.Option(retype(children)...)
|
||||
}
|
||||
|
||||
func P(children ...H) H {
|
||||
return gh.P(retype(children)...)
|
||||
}
|
||||
|
||||
func Picture(children ...H) H {
|
||||
return gh.Picture(retype(children)...)
|
||||
}
|
||||
|
||||
func Pre(children ...H) H {
|
||||
return gh.Pre(retype(children)...)
|
||||
}
|
||||
|
||||
func Progress(children ...H) H {
|
||||
return gh.Progress(retype(children)...)
|
||||
}
|
||||
|
||||
func Q(children ...H) H {
|
||||
return gh.Q(retype(children)...)
|
||||
}
|
||||
|
||||
func S(children ...H) H {
|
||||
return gh.S(retype(children)...)
|
||||
}
|
||||
|
||||
func Samp(children ...H) H {
|
||||
return gh.Samp(retype(children)...)
|
||||
}
|
||||
|
||||
func Script(children ...H) H {
|
||||
return gh.Script(retype(children)...)
|
||||
}
|
||||
|
||||
func Section(children ...H) H {
|
||||
return gh.Section(retype(children)...)
|
||||
}
|
||||
|
||||
func Select(children ...H) H {
|
||||
return gh.Select(retype(children)...)
|
||||
}
|
||||
|
||||
func Small(children ...H) H {
|
||||
return gh.Small(retype(children)...)
|
||||
}
|
||||
|
||||
func Source(children ...H) H {
|
||||
return gh.Source(retype(children)...)
|
||||
}
|
||||
|
||||
func Span(children ...H) H {
|
||||
return gh.Span(retype(children)...)
|
||||
}
|
||||
|
||||
func Strong(children ...H) H {
|
||||
return gh.Strong(retype(children)...)
|
||||
}
|
||||
|
||||
func Style(v string) H {
|
||||
return gh.Style(v)
|
||||
}
|
||||
|
||||
func Sub(children ...H) H {
|
||||
return gh.Sub(retype(children)...)
|
||||
}
|
||||
|
||||
func Summary(children ...H) H {
|
||||
return gh.Summary(retype(children)...)
|
||||
}
|
||||
|
||||
func Sup(children ...H) H {
|
||||
return gh.Sup(retype(children)...)
|
||||
}
|
||||
|
||||
func Table(children ...H) H {
|
||||
return gh.Table(retype(children)...)
|
||||
}
|
||||
|
||||
func TBody(children ...H) H {
|
||||
return gh.TBody(retype(children)...)
|
||||
}
|
||||
|
||||
func Td(children ...H) H {
|
||||
return gh.Td(retype(children)...)
|
||||
}
|
||||
|
||||
func Template(children ...H) H {
|
||||
return gh.Template(retype(children)...)
|
||||
}
|
||||
|
||||
func Textarea(children ...H) H {
|
||||
return gh.Textarea(retype(children)...)
|
||||
}
|
||||
|
||||
func TFoot(children ...H) H {
|
||||
return gh.TFoot(retype(children)...)
|
||||
}
|
||||
|
||||
func Th(children ...H) H {
|
||||
return gh.Th(retype(children)...)
|
||||
}
|
||||
|
||||
func THead(children ...H) H {
|
||||
return gh.THead(retype(children)...)
|
||||
}
|
||||
|
||||
func Time(children ...H) H {
|
||||
return gh.Time(retype(children)...)
|
||||
}
|
||||
|
||||
func Title(v string) H {
|
||||
return gh.Title(v)
|
||||
}
|
||||
|
||||
func Tr(children ...H) H {
|
||||
return gh.Tr(retype(children)...)
|
||||
}
|
||||
|
||||
func U(children ...H) H {
|
||||
return gh.U(retype(children)...)
|
||||
}
|
||||
|
||||
func Ul(children ...H) H {
|
||||
return gh.Ul(retype(children)...)
|
||||
}
|
||||
|
||||
func Var(children ...H) H {
|
||||
return gh.Var(retype(children)...)
|
||||
}
|
||||
|
||||
func Video(children ...H) H {
|
||||
return gh.Video(retype(children)...)
|
||||
}
|
||||
|
||||
func Wbr(children ...H) H {
|
||||
return gh.Wbr(retype(children)...)
|
||||
}
|
||||
78
h/h.go
Normal file
78
h/h.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// Package h provides a Go-native DSL for HTML composition.
|
||||
// Every element, attribute, and text node is constructed as a function that returns a [h.H] DOM node.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// h.Div(
|
||||
// h.H1(h.Text("Hello, Via")),
|
||||
// h.P(h.Text("Pure Go. No tmplates.")),
|
||||
// )
|
||||
package h
|
||||
|
||||
import (
|
||||
"io"
|
||||
g "maragu.dev/gomponents"
|
||||
gc "maragu.dev/gomponents/components"
|
||||
)
|
||||
|
||||
// H represents a DOM node.
|
||||
type H interface {
|
||||
Render(w io.Writer) error
|
||||
}
|
||||
|
||||
// Text creates a text DOM node that Renders the escaped string t.
|
||||
func Text(t string) H {
|
||||
return g.Text(t)
|
||||
}
|
||||
|
||||
// Textf creates a text DOM node that Renders the interpolated and escaped string format.
|
||||
func Textf(format string, a ...any) H {
|
||||
return g.Textf(format, a...)
|
||||
}
|
||||
|
||||
// / Raw creates a text DOM [Node] that just Renders the unescaped string t.
|
||||
func Raw(s string) H {
|
||||
return g.Raw(s)
|
||||
}
|
||||
|
||||
// Attr creates an attribute DOM [Node] with a name and optional value.
|
||||
// If only a name is passed, it's a name-only (boolean) attribute (like "required").
|
||||
// If a name and value are passed, it's a name-value attribute (like `class="header"`).
|
||||
// More than one value make [Attr] panic.
|
||||
// Use this if no convenience creator exists in the h package.
|
||||
func Attr(name string, value ...string) H {
|
||||
return g.Attr(name, value...)
|
||||
}
|
||||
|
||||
// HTML5Props defines properties for HTML5 pages. Title is set always set, Description
|
||||
// and Language elements only if the strings are non-empty.
|
||||
type HTML5Props struct {
|
||||
Title string
|
||||
Description string
|
||||
Language string
|
||||
Head []H
|
||||
Body []H
|
||||
HTMLAttrs []H
|
||||
}
|
||||
|
||||
// HTML5 document template.
|
||||
func HTML5(p HTML5Props) H {
|
||||
gp := gc.HTML5Props{
|
||||
Title: p.Title,
|
||||
Description: p.Description,
|
||||
Language: p.Language,
|
||||
Head: retype(p.Head),
|
||||
Body: retype(p.Body),
|
||||
HTMLAttrs: retype(p.HTMLAttrs),
|
||||
}
|
||||
gp.Head = append(gp.Head, Script(Type("module"), Src("/_datastar.js")))
|
||||
return gc.HTML5(gp)
|
||||
}
|
||||
|
||||
// JoinAttrs with the given name only on the first level of the given nodes. This means that
|
||||
// attributes on non-direct descendants are ignored. Attribute values are joined by spaces.
|
||||
//
|
||||
// Note that this renders all first-level attributes to check whether they should be processed.
|
||||
func JoinAttrs(name string, children ...H) H {
|
||||
return gc.JoinAttrs(name, retype(children)...)
|
||||
}
|
||||
13
h/util.go
Normal file
13
h/util.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package h
|
||||
|
||||
import (
|
||||
g "maragu.dev/gomponents"
|
||||
)
|
||||
|
||||
func retype(nodes []H) []g.Node {
|
||||
list := make([]g.Node, len(nodes))
|
||||
for i, node := range nodes {
|
||||
list[i] = node.(g.Node)
|
||||
}
|
||||
return list
|
||||
}
|
||||
96
internal/examples/counter/main.go
Normal file
96
internal/examples/counter/main.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/go-via/via"
|
||||
"github.com/go-via/via/h"
|
||||
)
|
||||
|
||||
type CounterState struct{ Count int }
|
||||
|
||||
func main() {
|
||||
v := via.New()
|
||||
|
||||
v.Page("/", func(c *via.Context) {
|
||||
|
||||
s := CounterState{Count: 0}
|
||||
|
||||
step := c.Signal(1)
|
||||
greeting := c.Signal("Hello...")
|
||||
|
||||
increment := c.Action(func() {
|
||||
s.Count += step.Int()
|
||||
c.Sync()
|
||||
})
|
||||
greetBob := c.Action(func() {
|
||||
greeting.SetValue("Hello Bob!")
|
||||
c.SyncSignals()
|
||||
})
|
||||
greetAlice := c.Action(func() {
|
||||
greeting.SetValue("Hello Alice!")
|
||||
c.SyncSignals()
|
||||
})
|
||||
|
||||
c.View(func() h.H {
|
||||
return h.Div(
|
||||
h.P(h.Textf("Count: %d", s.Count)),
|
||||
h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
|
||||
h.Label(
|
||||
h.Text("Update Step: "),
|
||||
h.Input(h.Type("number"), step.Bind()),
|
||||
),
|
||||
h.Button(h.Text("Increment"), increment.OnClick()),
|
||||
|
||||
h.P(h.Span(h.Text("Greeting: ")), h.Span(greeting.Text())),
|
||||
h.Button(h.Text("Greet Alice"), greetBob.OnClick()),
|
||||
h.Button(h.Text("Greet Alice"), greetAlice.OnClick()),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
v.Start(":3000")
|
||||
}
|
||||
|
||||
//
|
||||
// c.View(func() h.H {
|
||||
// return Layout(
|
||||
// h.Div(
|
||||
// h.Meta(h.Data("init", "@get('/_sse')")),
|
||||
// h.P(h.Data("text", "$via-ctx")),
|
||||
// h.Div(
|
||||
// counter(),
|
||||
// h.Data("signals:step", "1"),
|
||||
// h.Label(h.Text("Step")),
|
||||
// h.Input(h.Data("bind", "step")),
|
||||
// h.Button(
|
||||
// h.Text("Trigger foo"),
|
||||
// h.Data("on:click", "@get('/_action/foo')"),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// })
|
||||
|
||||
// conterComponent := c.Component("counter1", CounterComponent)
|
||||
//
|
||||
// in c.View of page add CounterComponent
|
||||
//
|
||||
// func CounterComponent(c *via.Context){
|
||||
// s := CounterState{ Count: 1 }
|
||||
// step := c.Signal(1)
|
||||
//
|
||||
// c.View(func() h.H {
|
||||
// return h.Div(
|
||||
// h.P(h.Textf("Count: %d", s.Count)),
|
||||
// h.Label(
|
||||
// h.Text("Step"),
|
||||
// h.Input(h.Type("number"), step.Bind()),
|
||||
// ),
|
||||
// h.Button(h.Text("Increment"), h.OnClick("inc")),
|
||||
// )
|
||||
// })
|
||||
//
|
||||
// c.Action("inc", func() {
|
||||
// s.Count += step
|
||||
// c.Sync()
|
||||
// })
|
||||
// }
|
||||
1
internal/examples/countercomp/main.go
Normal file
1
internal/examples/countercomp/main.go
Normal file
@@ -0,0 +1 @@
|
||||
package main
|
||||
186
signal.go
Normal file
186
signal.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package via
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-via/via/h"
|
||||
)
|
||||
|
||||
// Signal represents a value that is reactive in the browser. Signals
|
||||
// are synct with the server right before an action triggers.
|
||||
//
|
||||
// Use Bind() to connect a signal to an input and Text() to display it
|
||||
// reactively on an html element.
|
||||
type signal struct {
|
||||
id string
|
||||
v reflect.Value
|
||||
t reflect.Type
|
||||
changed bool
|
||||
err error
|
||||
}
|
||||
|
||||
// ID returns the signal ID
|
||||
func (s *signal) ID() string {
|
||||
return s.id
|
||||
}
|
||||
|
||||
// Err returns a signal error or nil if it contains no error.
|
||||
//
|
||||
// It is useful to check for errors after updating signals with
|
||||
// dinamic values.
|
||||
func (s *signal) Err() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
// Bind binds this signal to an imput element. When the imput changes
|
||||
// its value the signal updates in real-time in the browser.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// h.Input(h.Type("number"), mysignal.Bind())
|
||||
func (s *signal) Bind() h.H {
|
||||
return h.Data("bind", s.id)
|
||||
}
|
||||
|
||||
// Text binds the signal value to an html element as text.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// h.Div(h.Text("x: "), mysignal.Text())
|
||||
func (s *signal) Text() h.H {
|
||||
return h.Data("text", "$"+s.id)
|
||||
}
|
||||
|
||||
// SetValue updates the signal’s value and marks it for synchronization with the browser.
|
||||
// The change will be propagated to the browser using *Context.Sync() or *Context.SyncSignals().
|
||||
func (s *signal) SetValue(v any) {
|
||||
val := reflect.ValueOf(v)
|
||||
typ := reflect.TypeOf(v)
|
||||
if typ != s.t {
|
||||
s.err = fmt.Errorf("expected type '%s', got '%s'", s.t.String(), typ.String())
|
||||
return
|
||||
}
|
||||
s.v = val
|
||||
s.changed = true
|
||||
s.err = nil
|
||||
}
|
||||
|
||||
// String return the signal value as a string.
|
||||
func (s *signal) String() string {
|
||||
return fmt.Sprintf("%v", s.v)
|
||||
}
|
||||
|
||||
// Bool tries to read the signal value as a bool.
|
||||
// Returns the value or false on failure.
|
||||
func (s *signal) Bool() bool {
|
||||
switch s.v.Kind() {
|
||||
case reflect.Bool:
|
||||
return s.v.Bool()
|
||||
case reflect.String:
|
||||
val := strings.ToLower(s.v.String())
|
||||
return val == "true" || val == "1" || val == "yes" || val == "on"
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return s.v.Int() != 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return s.v.Float() != 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Int tries to read the signal value as an int.
|
||||
// Returns the value or 0 on failure.
|
||||
func (s *signal) Int() int {
|
||||
if n, err := strconv.Atoi(s.v.String()); err == nil {
|
||||
return n
|
||||
}
|
||||
if s.v.CanInt() {
|
||||
return int(s.v.Int())
|
||||
}
|
||||
if s.v.CanFloat() {
|
||||
return int(s.v.Float())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Int64 tries to read the signal value as an int64.
|
||||
// Returns the value or 0 on failure.
|
||||
func (s *signal) Int64() int64 {
|
||||
if n, err := strconv.ParseInt(s.v.String(), 10, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
if s.v.CanInt() {
|
||||
return s.v.Int()
|
||||
}
|
||||
if s.v.CanFloat() {
|
||||
return int64(s.v.Float())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Uint64 tries to read the signal value as an uint64.
|
||||
// Returns the value or 0 on failure.
|
||||
func (s *signal) Uint64() uint64 {
|
||||
if n, err := strconv.ParseUint(s.v.String(), 10, 64); err == nil {
|
||||
return n
|
||||
}
|
||||
if s.v.CanUint() {
|
||||
return s.v.Uint()
|
||||
}
|
||||
if s.v.CanFloat() {
|
||||
return uint64(s.v.Float())
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Float64 tries to read the signal value as a float64.
|
||||
// Returns the value or 0.0 on failure.
|
||||
func (s *signal) Float64() float64 {
|
||||
if n, err := strconv.ParseFloat(s.v.String(), 64); err == nil {
|
||||
return n
|
||||
}
|
||||
if s.v.CanFloat() {
|
||||
return s.v.Float()
|
||||
}
|
||||
if s.v.CanInt() {
|
||||
return float64(s.v.Int())
|
||||
}
|
||||
return 0.0
|
||||
}
|
||||
|
||||
// Complex128 tries to read the signal value as a complex128.
|
||||
// Returns the value or 0 on failure.
|
||||
func (s *signal) Complex128() complex128 {
|
||||
if s.v.Kind() == reflect.Complex128 {
|
||||
return s.v.Complex()
|
||||
}
|
||||
if s.v.Kind() == reflect.String {
|
||||
if n, err := strconv.ParseComplex(s.v.String(), 128); err == nil {
|
||||
return n
|
||||
}
|
||||
}
|
||||
if s.v.CanFloat() {
|
||||
return complex(s.v.Float(), 0)
|
||||
}
|
||||
if s.v.CanInt() {
|
||||
return complex(float64(s.v.Int()), 0)
|
||||
}
|
||||
return complex(0, 0)
|
||||
}
|
||||
|
||||
// Bytes tries to read the signal value as a []byte
|
||||
// Returns the value or an empty []byte on failure.
|
||||
func (s *signal) Bytes() []byte {
|
||||
switch s.v.Kind() {
|
||||
case reflect.Slice:
|
||||
if s.v.Type().Elem().Kind() == reflect.Uint8 {
|
||||
return s.v.Bytes()
|
||||
}
|
||||
case reflect.String:
|
||||
return []byte(s.v.String())
|
||||
}
|
||||
return make([]byte, 0)
|
||||
}
|
||||
224
via.go
Normal file
224
via.go
Normal file
@@ -0,0 +1,224 @@
|
||||
// Package via provides a reactive web framework for Go.
|
||||
// It lets you build live, type-safe web interfaces without JavaScript.
|
||||
//
|
||||
// Via unifies routing, state, and UI reactivity through a simple mental model:
|
||||
// Go on the server — HTML in the browser — updated in real time via Datastar.
|
||||
package via
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/go-via/via/h"
|
||||
"github.com/starfederation/datastar-go/datastar"
|
||||
)
|
||||
|
||||
//go:embed datastar.js
|
||||
var datastarJS []byte
|
||||
|
||||
type config struct {
|
||||
logLvl LogLevel
|
||||
}
|
||||
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
LogLevelError LogLevel = iota
|
||||
LogLevelWarn
|
||||
LogLevelInfo
|
||||
LogLevelDebug
|
||||
)
|
||||
|
||||
// via is the root application.
|
||||
// It manages page routing, user sessions, and SSE connections for live updates.
|
||||
type via struct {
|
||||
cfg config
|
||||
mux *http.ServeMux
|
||||
contextRegistry map[string]*Context
|
||||
contextRegistryMutex sync.RWMutex
|
||||
baseLayout func(h.HTML5Props) h.H
|
||||
}
|
||||
|
||||
func (v *via) logErr(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
log.Printf("[error] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func (v *via) logWarn(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
if v.cfg.logLvl <= LogLevelWarn {
|
||||
log.Printf("[warn] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *via) logInfo(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
if v.cfg.logLvl >= LogLevelInfo {
|
||||
log.Printf("[info] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *via) logDebug(c *Context, format string, a ...any) {
|
||||
cRef := ""
|
||||
if c != nil && c.id != "" {
|
||||
cRef = fmt.Sprintf("via-ctx=%q ", c.id)
|
||||
}
|
||||
if v.cfg.logLvl == LogLevelDebug {
|
||||
log.Printf("[debug] %smsg=%q", cRef, fmt.Sprintf(format, a...))
|
||||
}
|
||||
}
|
||||
|
||||
// Page registers a route and its associated page handler.
|
||||
// The handler receives a *Context to define UI, signals, and actions.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// v.Page("/", func(c *via.Context) {
|
||||
// c.View(func() h.H {
|
||||
// return h.H1(h.Text("Hello, Via!"))
|
||||
// })
|
||||
// })
|
||||
func (v *via) Page(route string, composeContext func(c *Context)) {
|
||||
v.mux.HandleFunc("GET "+route, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
id := fmt.Sprintf("%s_/%s", route, genRandID())
|
||||
c := newContext(id, v)
|
||||
v.logDebug(c, "GET %s", route)
|
||||
composeContext(c)
|
||||
v.registerCtx(c.id, c)
|
||||
// viewFn := c.view
|
||||
// viewFnWithID := func() h.H {
|
||||
// return h.Div(h.ID(c.id), viewFn())
|
||||
// }
|
||||
// c.view = viewFnWithID
|
||||
view := v.baseLayout(h.HTML5Props{
|
||||
Head: []h.H{
|
||||
h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))),
|
||||
h.Meta(h.Data("init", "@get('/_sse')")),
|
||||
},
|
||||
Body: []h.H{h.Div(h.ID(c.id))},
|
||||
})
|
||||
_ = view.Render(w)
|
||||
}))
|
||||
}
|
||||
|
||||
func (v *via) registerCtx(id string, c *Context) {
|
||||
v.contextRegistryMutex.Lock()
|
||||
defer v.contextRegistryMutex.Unlock()
|
||||
v.contextRegistry[id] = c
|
||||
}
|
||||
|
||||
// func (a *App) unregisterCtx(id string) {
|
||||
// if _, ok := a.contextRegistry[id]; ok {
|
||||
// a.contextRegistryMutex.Lock()
|
||||
// defer a.contextRegistryMutex.Unlock()
|
||||
// delete(a.contextRegistry, id)
|
||||
// }
|
||||
// }
|
||||
|
||||
func (v *via) getCtx(id string) (*Context, error) {
|
||||
if c, ok := v.contextRegistry[id]; ok {
|
||||
return c, nil
|
||||
}
|
||||
return nil, fmt.Errorf("ctx '%s' not found", id)
|
||||
}
|
||||
|
||||
// Start starts the Via HTTP server on the given address.
|
||||
func (v *via) Start(addr string) {
|
||||
v.logInfo(nil, "via started")
|
||||
log.Fatalf("via failed: %v", http.ListenAndServe(addr, v.mux))
|
||||
}
|
||||
|
||||
// New creates a new Via application with default configuration.
|
||||
func New() *via {
|
||||
mux := http.NewServeMux()
|
||||
app := &via{
|
||||
mux: mux,
|
||||
contextRegistry: make(map[string]*Context),
|
||||
cfg: config{logLvl: LogLevelDebug},
|
||||
baseLayout: h.HTML5,
|
||||
}
|
||||
|
||||
app.mux.HandleFunc("GET /favicon.ico", func(w http.ResponseWriter, r *http.Request) {})
|
||||
|
||||
app.mux.HandleFunc("GET /_datastar.js", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/javascript")
|
||||
_, _ = w.Write(datastarJS)
|
||||
})
|
||||
|
||||
app.mux.HandleFunc("GET /_sse", func(w http.ResponseWriter, r *http.Request) {
|
||||
var sigs map[string]any
|
||||
_ = datastar.ReadSignals(r, &sigs)
|
||||
cID, _ := sigs["via-ctx"].(string)
|
||||
c, err := app.getCtx(cID)
|
||||
if err != nil {
|
||||
app.logErr(nil, "failed to render page: %v", err)
|
||||
return
|
||||
}
|
||||
c.sse = datastar.NewSSE(w, r)
|
||||
app.logDebug(c, "SSE connection established")
|
||||
c.Sync()
|
||||
<-c.sse.Context().Done()
|
||||
c.sse = nil
|
||||
app.logDebug(c, "SSE connection closed")
|
||||
})
|
||||
app.mux.HandleFunc("GET /_action/{id}", func(w http.ResponseWriter, r *http.Request) {
|
||||
actionID := r.PathValue("id")
|
||||
var sigs map[string]any
|
||||
_ = datastar.ReadSignals(r, &sigs)
|
||||
cID, _ := sigs["via-ctx"].(string)
|
||||
app.logDebug(nil, "GET /_action/%s via-ctx=%s", actionID, cID)
|
||||
active_ctx_count := 0
|
||||
inactive_ctx_count := 0
|
||||
for _, c := range app.contextRegistry {
|
||||
if c.sse != nil {
|
||||
active_ctx_count++
|
||||
continue
|
||||
}
|
||||
inactive_ctx_count++
|
||||
}
|
||||
app.logDebug(nil, "active_ctx_count=%d inactive_ctx_count=%d", active_ctx_count, inactive_ctx_count)
|
||||
c, err := app.getCtx(cID)
|
||||
if err != nil {
|
||||
app.logErr(nil, "action '%s' failed: %v", actionID, err)
|
||||
return
|
||||
}
|
||||
actionFn, err := c.getActionFn(actionID)
|
||||
if err != nil {
|
||||
app.logDebug(c, "action '%s' failed: %v", actionID, err)
|
||||
return
|
||||
}
|
||||
// log err if actionFn panics
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
app.logErr(c, "action '%s' failed: %v", actionID, r)
|
||||
}
|
||||
}()
|
||||
c.signalsMux.Lock()
|
||||
defer c.signalsMux.Unlock()
|
||||
app.logDebug(c, "signals=%v", sigs)
|
||||
c.injectSignals(sigs)
|
||||
actionFn()
|
||||
|
||||
})
|
||||
return app
|
||||
}
|
||||
|
||||
func genRandID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
return hex.EncodeToString(b)[:8]
|
||||
}
|
||||
Reference in New Issue
Block a user