From 23aebf73f2666752d2fbea19d5fe3066423e2e67 Mon Sep 17 00:00:00 2001 From: Joao Goncalves Date: Mon, 3 Nov 2025 00:41:05 -0100 Subject: [PATCH] feat: improve component support --- context.go | 148 +++++++++++++++++++------- internal/examples/countercomp/main.go | 67 ++---------- via.go | 6 -- 3 files changed, 117 insertions(+), 104 deletions(-) diff --git a/context.go b/context.go index 9a64c1f..05516a2 100644 --- a/context.go +++ b/context.go @@ -16,16 +16,16 @@ import ( // // It binds user state and actions, manages reactive signals, and defines UI through View. type Context struct { - id string - app *via - view func() h.H - components map[string]*Context - componentsMux sync.RWMutex - sse *datastar.ServerSentEventGenerator - actionRegistry map[string]func() - signals map[string]*signal - signalsMux sync.Mutex - createdAt time.Time + id string + app *via + view func() h.H + componentRegistry map[string]*Context + parentPageCtx *Context + 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. @@ -40,25 +40,64 @@ func (c *Context) View(f func() h.H) { c.view = func() h.H { return h.Div(h.ID(c.id), f()) } } -// Component registers a sub context that is self contained self contained with it's own -// view, state actions and signals and returns the DOM node that can be added to the view -// of the parent. +// Component registers a sub context that has self contained data, actions and signals. +// It returns the component's view as a DOM node fn that can be placed in the view +// of the parent. Components can be added to components. +// +// Example: +// +// counterComponent := func(c *via.Context) { +// count := 0 +// step := c.Signal(1) +// +// increment := c.Action(func() { +// count += step.Int() +// c.Sync() +// }) +// +// c.View(func() h.H { +// return h.Div( +// h.P(h.Textf("Count: %d", 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()), +// ) +// }) +// }) +// +// v.Page("/", func(c *via.Context) { +// counter1 := c.Component(counterComponent) +// counter2 := c.Component(counterComponent) +// +// c.View(func() h.H { +// return h.Div( +// h.H1(h.Text("Counter 1")), +// counter1(), +// h.H1(h.Text("Counter 2")), +// counter2(), +// ) +// }) +// }) func (c *Context) Component(f func(c *Context)) func() h.H { id := c.id + "/_component/" + genRandID() compCtx := newContext(id, c.app) + if c.isComponent() { + compCtx.parentPageCtx = c.parentPageCtx + } else { + compCtx.parentPageCtx = c + } f(compCtx) - compCtx.sse = c.sse - // c.componentsMux.Lock() - // defer c.componentsMux.Unlock() - // - // c.components[id] = compCtx - c.app.contextRegistryMutex.Lock() - defer c.app.contextRegistryMutex.Unlock() - c.app.contextRegistry[id] = compCtx - + c.componentRegistry[id] = compCtx return compCtx.view } +func (c *Context) isComponent() bool { + return c.parentPageCtx != nil +} + // Action registers an event handler and returns a trigger to that event that // that can be added to the view fn as any other via.h element. // @@ -77,15 +116,17 @@ func (c *Context) Component(f func(c *Context)) func() h.H { // ) // }) 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 + + if c.isComponent() { + c.parentPageCtx.actionRegistry[id] = f + } else { + c.actionRegistry[id] = f + } return &actionTrigger{id} } @@ -137,7 +178,13 @@ func (c *Context) Signal(v any) *signal { t: reflect.TypeOf(v), changed: true, } - c.signals[sigID] = sig + + // components register signals on parent page + if c.isComponent() { + c.parentPageCtx.signals[sigID] = sig + } else { + c.signals[sigID] = sig + } return sig } @@ -159,15 +206,22 @@ func (c *Context) injectSignals(sigs map[string]any) { // Sync pushes the current view state and signal changes to the browser immediately // over the live SSE event stream. func (c *Context) Sync() { - if c.sse == nil { - c.app.logErr(c, "sync view failed: no sse connection") + // components use parent page sse stream + var sse *datastar.ServerSentEventGenerator + if c.isComponent() { + sse = c.parentPageCtx.sse + } else { + sse = c.sse + } + if sse == nil { + c.app.logErr(c, "sync view failed: inactive SSE stream") } 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()) + _ = sse.PatchElements(elemsPatch.String()) updatedSigs := make(map[string]any) for id, sig := range c.signals { if sig.err != nil { @@ -178,7 +232,7 @@ func (c *Context) Sync() { } } if len(updatedSigs) != 0 { - _ = c.sse.MarshalAndPatchSignals(updatedSigs) + _ = sse.MarshalAndPatchSignals(updatedSigs) } } @@ -200,7 +254,13 @@ func (c *Context) Sync() { // Then, the merge will only occur if the ID of the top level element in the patch // matches 'my-element'. func (c *Context) SyncElements(elem h.H) { - if c.sse == nil { + var sse *datastar.ServerSentEventGenerator + if c.isComponent() { + sse = c.parentPageCtx.sse + } else { + sse = c.sse + } + if sse == nil { c.app.logErr(c, "sync element failed: no sse connection") } if c.view == nil { @@ -213,13 +273,19 @@ func (c *Context) SyncElements(elem h.H) { } b := bytes.NewBuffer(make([]byte, 0)) _ = elem.Render(b) - c.sse.PatchElements(b.String()) + _ = sse.PatchElements(b.String()) } // SyncSignals pushes the current signal changes to the browser immediately // over the live SSE event stream. func (c *Context) SyncSignals() { - if c.sse == nil { + var sse *datastar.ServerSentEventGenerator + if c.isComponent() { + sse = c.parentPageCtx.sse + } else { + sse = c.sse + } + if sse == nil { c.app.logErr(c, "sync signals failed: sse connection not found") } updatedSigs := make(map[string]any) @@ -232,7 +298,7 @@ func (c *Context) SyncSignals() { } } if len(updatedSigs) != 0 { - _ = c.sse.MarshalAndPatchSignals(updatedSigs) + _ = sse.MarshalAndPatchSignals(updatedSigs) } } @@ -242,11 +308,11 @@ func newContext(id string, a *via) *Context { } return &Context{ - id: id, - app: a, - components: make(map[string]*Context), - actionRegistry: make(map[string]func()), - signals: make(map[string]*signal), - createdAt: time.Now(), + id: id, + app: a, + componentRegistry: make(map[string]*Context), + actionRegistry: make(map[string]func()), + signals: make(map[string]*signal), + createdAt: time.Now(), } } diff --git a/internal/examples/countercomp/main.go b/internal/examples/countercomp/main.go index 6af20e3..273c5c5 100644 --- a/internal/examples/countercomp/main.go +++ b/internal/examples/countercomp/main.go @@ -9,13 +9,15 @@ func main() { v := via.New() v.Page("/", func(c *via.Context) { - counter1 := c.Component(counterComponent) - counter2 := c.Component(counterComponent) + counterComp1 := c.Component(counterCompFn) + counterComp2 := c.Component(counterCompFn) c.View(func() h.H { return h.Div( - counter1(), - counter2(), + h.H1(h.Text("Counter 1")), + counterComp1(), + h.H1(h.Text("Counter 2")), + counterComp2(), ) }) }) @@ -23,22 +25,18 @@ func main() { v.Start(":3000") } -type Counter struct{ Count int } - -func counterComponent(c *via.Context) { - - s := Counter{Count: 0} - +func counterCompFn(c *via.Context) { + count := 0 step := c.Signal(1) increment := c.Action(func() { - s.Count += step.Int() + count += step.Int() c.Sync() }) c.View(func() h.H { return h.Div( - h.P(h.Textf("Count: %d", s.Count)), + h.P(h.Textf("Count: %d", count)), h.P(h.Span(h.Text("Step: ")), h.Span(step.Text())), h.Label( h.Text("Update Step: "), @@ -48,48 +46,3 @@ func counterComponent(c *via.Context) { ) }) } - -// -// 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() -// }) -// } diff --git a/via.go b/via.go index 75dfd0d..2c16eb3 100644 --- a/via.go +++ b/via.go @@ -99,11 +99,6 @@ func (v *via) Page(route string, composeContext func(c *Context)) { 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))), @@ -180,7 +175,6 @@ func New() *via { 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 {