feat: add Redirect and ReplaceURL methods for browser navigation

Add SSE-based navigation methods to Context:
- Redirect(url) / Redirectf() - navigate to new page
- ReplaceURL(url) / ReplaceURLf() - update URL without navigation

Update session example to demonstrate full login flow with redirects.
This commit is contained in:
Ryan Hamamura
2026-01-12 00:47:52 -10:00
parent 9a23188973
commit 03b6d7453a
3 changed files with 97 additions and 17 deletions

View File

@@ -318,6 +318,37 @@ func (c *Context) ExecScript(s string) {
c.sendPatch(patch{patchTypeScript, s})
}
// Redirect navigates the browser to the given URL.
// This triggers a full page navigation - the current context will be disposed
// and a new context created at the destination URL.
func (c *Context) Redirect(url string) {
if url == "" {
c.app.logWarn(c, "redirect failed: empty url")
return
}
c.sendPatch(patch{patchTypeRedirect, url})
}
// Redirectf navigates the browser to a URL constructed from the format string and arguments.
func (c *Context) Redirectf(format string, a ...any) {
c.Redirect(fmt.Sprintf(format, a...))
}
// ReplaceURL updates the browser's URL and history without triggering navigation.
// Useful for updating query params or path to reflect UI state changes.
func (c *Context) ReplaceURL(url string) {
if url == "" {
c.app.logWarn(c, "replace url failed: empty url")
return
}
c.sendPatch(patch{patchTypeReplaceURL, url})
}
// ReplaceURLf updates the browser's URL using a format string.
func (c *Context) ReplaceURLf(format string, a ...any) {
c.ReplaceURL(fmt.Sprintf(format, a...))
}
// stopAllRoutines stops all go routines tied to this Context preventing goroutine leaks.
func (c *Context) stopAllRoutines() {
select {

View File

@@ -7,11 +7,11 @@ import (
func main() {
v := via.New()
v.Config(via.Options{ServerAddress: ":7331"})
v.Page("/", func(c *via.Context) {
username := c.Session().GetString("username")
// Login page
v.Page("/login", func(c *via.Context) {
flash := c.Session().PopString("flash")
usernameInput := c.Signal("")
login := c.Action(func() {
@@ -20,38 +20,69 @@ func main() {
c.Session().Set("username", name)
c.Session().Set("flash", "Welcome, "+name+"!")
c.Session().RenewToken()
c.Redirect("/dashboard")
}
c.Sync()
})
logout := c.Action(func() {
c.Session().Set("flash", "Goodbye!")
c.Session().Delete("username")
c.Sync()
})
c.View(func() h.H {
// Already logged in? Redirect to dashboard
if c.Session().GetString("username") != "" {
c.Redirect("/dashboard")
return h.Div()
}
var flashMsg h.H
if flash != "" {
flashMsg = h.P(h.Text(flash), h.Style("color: green"))
}
return h.Div(
flashMsg,
h.H1(h.Text("Login")),
h.Input(h.Type("text"), h.Placeholder("Username"), usernameInput.Bind()),
h.Button(h.Text("Login"), login.OnClick()),
)
})
})
// Dashboard page (protected)
v.Page("/dashboard", func(c *via.Context) {
logout := c.Action(func() {
c.Session().Set("flash", "Goodbye!")
c.Session().Delete("username")
c.Redirect("/login")
})
c.View(func() h.H {
username := c.Session().GetString("username")
// Not logged in? Redirect to login
if username == "" {
return h.Div(
flashMsg,
h.H1(h.Text("Login")),
h.Input(h.Type("text"), h.Placeholder("Username"), usernameInput.Bind()),
h.Button(h.Text("Login"), login.OnClick()),
)
c.Session().Set("flash", "Please log in first")
c.Redirect("/login")
return h.Div()
}
flash := c.Session().PopString("flash")
var flashMsg h.H
if flash != "" {
flashMsg = h.P(h.Text(flash), h.Style("color: green"))
}
return h.Div(
flashMsg,
h.H1(h.Textf("Hello, %s!", username)),
h.H1(h.Textf("Dashboard - Hello, %s!", username)),
h.P(h.Text("Your session persists across page refreshes.")),
h.Button(h.Text("Logout"), logout.OnClick()),
)
})
})
// Redirect root to login
v.Page("/", func(c *via.Context) {
c.View(func() h.H {
c.Redirect("/login")
return h.Div()
})
})
v.Start()
}

18
via.go
View File

@@ -15,6 +15,7 @@ import (
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
@@ -359,6 +360,8 @@ const (
patchTypeElements = iota
patchTypeSignals
patchTypeScript
patchTypeRedirect
patchTypeReplaceURL
)
type patch struct {
@@ -453,6 +456,21 @@ func New() *V {
v.logErr(c, "ExecuteScript failed: %v", err)
}
}
case patchTypeRedirect:
if err := sse.Redirect(patch.content); err != nil {
if sse.Context().Err() == nil {
v.logErr(c, "Redirect failed: %v", err)
}
}
case patchTypeReplaceURL:
parsedURL, err := url.Parse(patch.content)
if err != nil {
v.logErr(c, "ReplaceURL failed to parse URL: %v", err)
} else if err := sse.ReplaceURL(*parsedURL); err != nil {
if sse.Context().Err() == nil {
v.logErr(c, "ReplaceURL failed: %v", err)
}
}
}
}
}