feat: improve component support

This commit is contained in:
Joao Goncalves
2025-11-03 00:41:05 -01:00
parent c0e050b0f8
commit 23aebf73f2
3 changed files with 117 additions and 104 deletions

View File

@@ -19,8 +19,8 @@ type Context struct {
id string id string
app *via app *via
view func() h.H view func() h.H
components map[string]*Context componentRegistry map[string]*Context
componentsMux sync.RWMutex parentPageCtx *Context
sse *datastar.ServerSentEventGenerator sse *datastar.ServerSentEventGenerator
actionRegistry map[string]func() actionRegistry map[string]func()
signals map[string]*signal signals map[string]*signal
@@ -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()) } 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 // Component registers a sub context that has self contained data, actions and signals.
// view, state actions and signals and returns the DOM node that can be added to the view // It returns the component's view as a DOM node fn that can be placed in the view
// of the parent. // 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 { func (c *Context) Component(f func(c *Context)) func() h.H {
id := c.id + "/_component/" + genRandID() id := c.id + "/_component/" + genRandID()
compCtx := newContext(id, c.app) compCtx := newContext(id, c.app)
if c.isComponent() {
compCtx.parentPageCtx = c.parentPageCtx
} else {
compCtx.parentPageCtx = c
}
f(compCtx) f(compCtx)
compCtx.sse = c.sse c.componentRegistry[id] = compCtx
// 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
return compCtx.view 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 // 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. // 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 { func (c *Context) Action(f func()) *actionTrigger {
// if id == "" {
// c.app.logErr(c, "failed to bind action to context: id is ''")
// }
id := genRandID() id := genRandID()
if f == nil { if f == nil {
c.app.logErr(c, "failed to bind action '%s' to context: nil func", id) c.app.logErr(c, "failed to bind action '%s' to context: nil func", id)
return nil return nil
} }
if c.isComponent() {
c.parentPageCtx.actionRegistry[id] = f
} else {
c.actionRegistry[id] = f c.actionRegistry[id] = f
}
return &actionTrigger{id} return &actionTrigger{id}
} }
@@ -137,7 +178,13 @@ func (c *Context) Signal(v any) *signal {
t: reflect.TypeOf(v), t: reflect.TypeOf(v),
changed: true, changed: true,
} }
// components register signals on parent page
if c.isComponent() {
c.parentPageCtx.signals[sigID] = sig
} else {
c.signals[sigID] = sig c.signals[sigID] = sig
}
return 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 // Sync pushes the current view state and signal changes to the browser immediately
// over the live SSE event stream. // over the live SSE event stream.
func (c *Context) Sync() { func (c *Context) Sync() {
if c.sse == nil { // components use parent page sse stream
c.app.logErr(c, "sync view failed: no sse connection") 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)) elemsPatch := bytes.NewBuffer(make([]byte, 0))
if err := c.view().Render(elemsPatch); err != nil { if err := c.view().Render(elemsPatch); err != nil {
c.app.logErr(c, "sync view failed: %v", err) c.app.logErr(c, "sync view failed: %v", err)
return return
} }
_ = c.sse.PatchElements(elemsPatch.String()) _ = sse.PatchElements(elemsPatch.String())
updatedSigs := make(map[string]any) updatedSigs := make(map[string]any)
for id, sig := range c.signals { for id, sig := range c.signals {
if sig.err != nil { if sig.err != nil {
@@ -178,7 +232,7 @@ func (c *Context) Sync() {
} }
} }
if len(updatedSigs) != 0 { 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 // Then, the merge will only occur if the ID of the top level element in the patch
// matches 'my-element'. // matches 'my-element'.
func (c *Context) SyncElements(elem h.H) { 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") c.app.logErr(c, "sync element failed: no sse connection")
} }
if c.view == nil { if c.view == nil {
@@ -213,13 +273,19 @@ func (c *Context) SyncElements(elem h.H) {
} }
b := bytes.NewBuffer(make([]byte, 0)) b := bytes.NewBuffer(make([]byte, 0))
_ = elem.Render(b) _ = elem.Render(b)
c.sse.PatchElements(b.String()) _ = sse.PatchElements(b.String())
} }
// SyncSignals pushes the current signal changes to the browser immediately // SyncSignals pushes the current signal changes to the browser immediately
// over the live SSE event stream. // over the live SSE event stream.
func (c *Context) SyncSignals() { 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") c.app.logErr(c, "sync signals failed: sse connection not found")
} }
updatedSigs := make(map[string]any) updatedSigs := make(map[string]any)
@@ -232,7 +298,7 @@ func (c *Context) SyncSignals() {
} }
} }
if len(updatedSigs) != 0 { if len(updatedSigs) != 0 {
_ = c.sse.MarshalAndPatchSignals(updatedSigs) _ = sse.MarshalAndPatchSignals(updatedSigs)
} }
} }
@@ -244,7 +310,7 @@ func newContext(id string, a *via) *Context {
return &Context{ return &Context{
id: id, id: id,
app: a, app: a,
components: make(map[string]*Context), componentRegistry: make(map[string]*Context),
actionRegistry: make(map[string]func()), actionRegistry: make(map[string]func()),
signals: make(map[string]*signal), signals: make(map[string]*signal),
createdAt: time.Now(), createdAt: time.Now(),

View File

@@ -9,13 +9,15 @@ func main() {
v := via.New() v := via.New()
v.Page("/", func(c *via.Context) { v.Page("/", func(c *via.Context) {
counter1 := c.Component(counterComponent) counterComp1 := c.Component(counterCompFn)
counter2 := c.Component(counterComponent) counterComp2 := c.Component(counterCompFn)
c.View(func() h.H { c.View(func() h.H {
return h.Div( return h.Div(
counter1(), h.H1(h.Text("Counter 1")),
counter2(), counterComp1(),
h.H1(h.Text("Counter 2")),
counterComp2(),
) )
}) })
}) })
@@ -23,22 +25,18 @@ func main() {
v.Start(":3000") v.Start(":3000")
} }
type Counter struct{ Count int } func counterCompFn(c *via.Context) {
count := 0
func counterComponent(c *via.Context) {
s := Counter{Count: 0}
step := c.Signal(1) step := c.Signal(1)
increment := c.Action(func() { increment := c.Action(func() {
s.Count += step.Int() count += step.Int()
c.Sync() c.Sync()
}) })
c.View(func() h.H { c.View(func() h.H {
return h.Div( 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.P(h.Span(h.Text("Step: ")), h.Span(step.Text())),
h.Label( h.Label(
h.Text("Update Step: "), 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()
// })
// }

6
via.go
View File

@@ -99,11 +99,6 @@ func (v *via) Page(route string, composeContext func(c *Context)) {
v.logDebug(c, "GET %s", route) v.logDebug(c, "GET %s", route)
composeContext(c) composeContext(c)
v.registerCtx(c.id, 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{ view := v.baseLayout(h.HTML5Props{
Head: []h.H{ Head: []h.H{
h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))), h.Meta(h.Data("signals", fmt.Sprintf("{'via-ctx':'%s'}", id))),
@@ -180,7 +175,6 @@ func New() *via {
var sigs map[string]any var sigs map[string]any
_ = datastar.ReadSignals(r, &sigs) _ = datastar.ReadSignals(r, &sigs)
cID, _ := sigs["via-ctx"].(string) cID, _ := sigs["via-ctx"].(string)
app.logDebug(nil, "GET /_action/%s via-ctx=%s", actionID, cID)
active_ctx_count := 0 active_ctx_count := 0
inactive_ctx_count := 0 inactive_ctx_count := 0
for _, c := range app.contextRegistry { for _, c := range app.contextRegistry {