feat: add path params; add pathparams example

This commit is contained in:
Joao Goncalves
2025-12-04 17:53:06 -01:00
parent 26268f698a
commit 81d44954a4
4 changed files with 137 additions and 8 deletions

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"maps"
"reflect" "reflect"
"sync" "sync"
"time" "time"
@@ -20,12 +21,13 @@ type Context struct {
route string route string
app *V app *V
view func() h.H view func() h.H
routeParams map[string]string
componentRegistry map[string]*Context componentRegistry map[string]*Context
parentPageCtx *Context parentPageCtx *Context
patchChan chan patch patchChan chan patch
actionRegistry map[string]func() actionRegistry map[string]func()
signals *sync.Map signals *sync.Map
mutex sync.RWMutex mu sync.RWMutex
ctxDisposedChan chan struct{} ctxDisposedChan chan struct{}
} }
@@ -170,8 +172,8 @@ func (c *Context) Signal(v any) *signal {
changed: true, changed: true,
} }
c.mutex.Lock() c.mu.Lock()
defer c.mutex.Unlock() defer c.mu.Unlock()
if c.isComponent() { // components register signals on parent page if c.isComponent() { // components register signals on parent page
c.parentPageCtx.signals.Store(sigID, sig) c.parentPageCtx.signals.Store(sigID, sig)
} else { } else {
@@ -187,8 +189,8 @@ func (c *Context) injectSignals(sigs map[string]any) {
return return
} }
c.mutex.Lock() c.mu.Lock()
defer c.mutex.Unlock() defer c.mu.Unlock()
for sigID, val := range sigs { for sigID, val := range sigs {
if _, ok := c.signals.Load(sigID); !ok { if _, ok := c.signals.Load(sigID); !ok {
@@ -218,8 +220,8 @@ func (c *Context) getPatchChan() chan patch {
} }
func (c *Context) prepareSignalsForPatch() map[string]any { func (c *Context) prepareSignalsForPatch() map[string]any {
c.mutex.RLock() c.mu.RLock()
defer c.mutex.RUnlock() defer c.mu.RUnlock()
updatedSigs := make(map[string]any) updatedSigs := make(map[string]any)
c.signals.Range(func(sigID, value any) bool { c.signals.Range(func(sigID, value any) bool {
if sig, ok := value.(*signal); ok { 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 { func newContext(id string, route string, v *V) *Context {
if v == nil { if v == nil {
log.Fatal("create context failed: app pointer is 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{ return &Context{
id: id, id: id,
route: route, route: route,
routeParams: make(map[string]string),
app: v, app: v,
componentRegistry: make(map[string]*Context), componentRegistry: make(map[string]*Context),
actionRegistry: make(map[string]func()), actionRegistry: make(map[string]func()),

View File

@@ -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(`<svg xmlns="http://www.w3.org/2000/svg" height="18" width="24.25" viewBox="0 0 496 512" class="icon-github"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"></path></svg>`),
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()
}

View File

@@ -46,7 +46,8 @@ func (r *OnIntervalRoutine) Stop() {
r.localInterrupt <- struct{}{} 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{ r := &OnIntervalRoutine{
ctxDisposed: ctxDisposedChan, ctxDisposed: ctxDisposedChan,
localInterrupt: make(chan struct{}), localInterrupt: make(chan struct{}),

20
via.go
View File

@@ -161,6 +161,8 @@ func (v *V) Page(route string, initContextFn func(c *Context)) {
} }
id := fmt.Sprintf("%s_/%s", route, genRandID()) id := fmt.Sprintf("%s_/%s", route, genRandID())
c := newContext(id, route, v) c := newContext(id, route, v)
routeParams := extractParams(route, r.URL.Path)
c.injectRouteParams(routeParams)
initContextFn(c) initContextFn(c)
v.registerCtx(c) v.registerCtx(c)
if v.cfg.DevMode { if v.cfg.DevMode {
@@ -484,3 +486,21 @@ func genRandID() string {
rand.Read(b) rand.Read(b)
return hex.EncodeToString(b)[:8] 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
}