feat: improve component support
This commit is contained in:
148
context.go
148
context.go
@@ -16,16 +16,16 @@ import (
|
|||||||
//
|
//
|
||||||
// It binds user state and actions, manages reactive signals, and defines UI through View.
|
// It binds user state and actions, manages reactive signals, and defines UI through View.
|
||||||
type Context struct {
|
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
|
||||||
signalsMux sync.Mutex
|
signalsMux sync.Mutex
|
||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// View defines the UI rendered by this context.
|
// 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()) }
|
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
|
||||||
}
|
}
|
||||||
c.actionRegistry[id] = f
|
|
||||||
|
if c.isComponent() {
|
||||||
|
c.parentPageCtx.actionRegistry[id] = f
|
||||||
|
} else {
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,11 +308,11 @@ 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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
6
via.go
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user