From 81d44954a446de48738b8b19a9ebbf850a550474 Mon Sep 17 00:00:00 2001 From: Joao Goncalves Date: Thu, 4 Dec 2025 17:53:06 -0100 Subject: [PATCH] feat: add path params; add pathparams example --- context.go | 53 ++++++++++++++++++--- internal/examples/pathparams/main.go | 69 ++++++++++++++++++++++++++++ routine.go | 3 +- via.go | 20 ++++++++ 4 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 internal/examples/pathparams/main.go diff --git a/context.go b/context.go index 19eff56..498d9e0 100644 --- a/context.go +++ b/context.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "log" + "maps" "reflect" "sync" "time" @@ -20,12 +21,13 @@ type Context struct { route string app *V view func() h.H + routeParams map[string]string componentRegistry map[string]*Context parentPageCtx *Context patchChan chan patch actionRegistry map[string]func() signals *sync.Map - mutex sync.RWMutex + mu sync.RWMutex ctxDisposedChan chan struct{} } @@ -170,8 +172,8 @@ func (c *Context) Signal(v any) *signal { changed: true, } - c.mutex.Lock() - defer c.mutex.Unlock() + c.mu.Lock() + defer c.mu.Unlock() if c.isComponent() { // components register signals on parent page c.parentPageCtx.signals.Store(sigID, sig) } else { @@ -187,8 +189,8 @@ func (c *Context) injectSignals(sigs map[string]any) { return } - c.mutex.Lock() - defer c.mutex.Unlock() + c.mu.Lock() + defer c.mu.Unlock() for sigID, val := range sigs { if _, ok := c.signals.Load(sigID); !ok { @@ -218,8 +220,8 @@ func (c *Context) getPatchChan() chan patch { } func (c *Context) prepareSignalsForPatch() map[string]any { - c.mutex.RLock() - defer c.mutex.RUnlock() + c.mu.RLock() + defer c.mu.RUnlock() updatedSigs := make(map[string]any) c.signals.Range(func(sigID, value any) bool { if sig, ok := value.(*signal); ok { @@ -322,6 +324,42 @@ func (c *Context) stopAllRoutines() { } } +func (c *Context) injectRouteParams(params map[string]string) { + if params == nil { + return + } + m := make(map[string]string) + c.mu.Lock() + defer c.mu.Unlock() + maps.Copy(m, params) + c.routeParams = m + +} + +// GetPathParam retrieves the value from the page request URL for the given parameter name +// or an empty string if not found. +// +// Example: +// +// v.Page("/users/{user_id}", func(c *via.Context) { +// +// userID := GetPathParam("user_id") +// +// c.View(func() h.H { +// return h.Div( +// h.H1(h.Textf("User ID: %s", userID)), +// ) +// }) +// }) +func (c *Context) GetPathParam(param string) string { + c.mu.RLock() + defer c.mu.RUnlock() + if p, ok := c.routeParams[param]; ok { + return p + } + return "" +} + func newContext(id string, route string, v *V) *Context { if v == nil { log.Fatal("create context failed: app pointer is nil") @@ -330,6 +368,7 @@ func newContext(id string, route string, v *V) *Context { return &Context{ id: id, route: route, + routeParams: make(map[string]string), app: v, componentRegistry: make(map[string]*Context), actionRegistry: make(map[string]func()), diff --git a/internal/examples/pathparams/main.go b/internal/examples/pathparams/main.go new file mode 100644 index 0000000..bbaf983 --- /dev/null +++ b/internal/examples/pathparams/main.go @@ -0,0 +1,69 @@ +package main + +import ( + "strconv" + + "github.com/go-via/via" + "github.com/go-via/via-plugin-picocss/picocss" + . "github.com/go-via/via/h" +) + +func main() { + v := via.New() + + v.Config(via.Options{ + Plugins: []via.Plugin{picocss.Default}, + }) + + v.Page("/counters/{counter_id}/{start_at_step}", func(c *via.Context) { + + counterID := c.GetPathParam("counter_id") + startAtStep, _ := strconv.Atoi(c.GetPathParam("start_at_step")) + + count := 0 + step := c.Signal(startAtStep) + + increment := c.Action(func() { + count += step.Int() + c.Sync() + }) + + c.View(func() H { + return Main(Class("container"), + + Nav( + Ul( + Li( + Strong(Text("⚡Via Example")), + ), + ), + Ul( + Li( + Raw(``), + A(Class("contrast"), + Text(" GitHub"), + Href("https://github.com/go-via/via"), + ), + ), + ), + ), + + Section( + Article( + H3(Text(counterID)), + Hr(), + H5(Textf("Count %d", count)), + H6(Text("Step "), step.Text()), + FieldSet(Role("group"), + Input(Type("number"), step.Bind()), + Button(Text("Increment"), increment.OnClick()), + ), + ), + ), + ) + }) + + }) + + v.Start() +} diff --git a/routine.go b/routine.go index b0826fd..03b4f9a 100644 --- a/routine.go +++ b/routine.go @@ -46,7 +46,8 @@ func (r *OnIntervalRoutine) Stop() { r.localInterrupt <- struct{}{} } -func newOnIntervalRoutine(ctxDisposedChan chan struct{}, duration time.Duration, handler func()) *OnIntervalRoutine { +func newOnIntervalRoutine(ctxDisposedChan chan struct{}, + duration time.Duration, handler func()) *OnIntervalRoutine { r := &OnIntervalRoutine{ ctxDisposed: ctxDisposedChan, localInterrupt: make(chan struct{}), diff --git a/via.go b/via.go index 4698a9b..6b37ad6 100644 --- a/via.go +++ b/via.go @@ -161,6 +161,8 @@ func (v *V) Page(route string, initContextFn func(c *Context)) { } id := fmt.Sprintf("%s_/%s", route, genRandID()) c := newContext(id, route, v) + routeParams := extractParams(route, r.URL.Path) + c.injectRouteParams(routeParams) initContextFn(c) v.registerCtx(c) if v.cfg.DevMode { @@ -484,3 +486,21 @@ func genRandID() string { rand.Read(b) return hex.EncodeToString(b)[:8] } + +func extractParams(pattern, path string) map[string]string { + p := strings.Split(strings.Trim(pattern, "/"), "/") + u := strings.Split(strings.Trim(path, "/"), "/") + if len(p) != len(u) { + return nil + } + params := make(map[string]string) + for i := range p { + if strings.HasPrefix(p[i], "{") && strings.HasSuffix(p[i], "}") { + key := p[i][1 : len(p[i])-1] // remove {} + params[key] = u[i] + } else if p[i] != u[i] { + continue + } + } + return params +}