Files
iris/navigation/router.go
Hugo Nijhuis 00d98879d3
Some checks failed
CI / build (push) Failing after 36s
Initial iris repository structure
WASM reactive UI framework for Go:
- reactive/ - Signal[T], Effect, Runtime
- ui/ - Button, Text, Input, View, Canvas, SVG components
- navigation/ - Router, guards, history management
- auth/ - OIDC client for WASM applications
- host/ - Static file server

Extracted from arcadia as open-source component.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-08 19:23:49 +01:00

158 lines
3.5 KiB
Go

package navigation
import (
"strings"
"git.flowmade.one/flowmade-one/iris/reactive"
"git.flowmade.one/flowmade-one/iris/ui"
)
type RouteGuard func(route *Route, params map[string]string) bool
type Route struct {
Path string
Handler func(params map[string]string) ui.View
Guards []RouteGuard
parts []string
params []string
}
type Router struct {
routes []Route
currentPath *reactive.Signal[string]
currentView *reactive.Signal[ui.View]
notFound func() ui.View
}
func NewRouter(routes []Route) *Router {
currentPath := reactive.NewSignal("")
currentView := reactive.NewSignal(ui.NewView())
router := &Router{
routes: make([]Route, len(routes)),
currentPath: &currentPath,
currentView: &currentView,
notFound: defaultNotFound,
}
// Parse route patterns
for i, route := range routes {
parsedRoute := route
parsedRoute.parts, parsedRoute.params = parseRoute(route.Path)
router.routes[i] = parsedRoute
}
return router
}
func (r *Router) SetNotFoundHandler(handler func() ui.View) {
r.notFound = handler
}
func (r *Router) Navigate(path string) {
r.currentPath.Set(path)
r.updateView(path)
}
func (r *Router) GetCurrentPath() string {
return r.currentPath.Get()
}
func (r *Router) GetCurrentView() ui.View {
return r.currentView.Get()
}
func (r *Router) updateView(path string) {
for _, route := range r.routes {
if params := r.matchRoute(&route, path); params != nil {
// Check guards
if r.checkGuards(&route, params) {
view := route.Handler(params)
r.currentView.Set(view)
return
}
}
}
// No route matched or guards failed
r.currentView.Set(r.notFound())
}
func (r *Router) matchRoute(route *Route, path string) map[string]string {
// Strip query parameters for route matching
if queryIndex := strings.Index(path, "?"); queryIndex != -1 {
path = path[:queryIndex]
}
pathParts := strings.Split(strings.Trim(path, "/"), "/")
if len(pathParts) == 1 && pathParts[0] == "" {
pathParts = []string{} // Handle root path
}
if len(pathParts) != len(route.parts) {
return nil
}
params := make(map[string]string)
paramIndex := 0
for i, routePart := range route.parts {
if strings.HasPrefix(routePart, ":") {
// This is a parameter
if paramIndex < len(route.params) {
params[route.params[paramIndex]] = pathParts[i]
paramIndex++
}
} else {
// Static part must match exactly
if routePart != pathParts[i] {
return nil
}
}
}
return params
}
func (r *Router) checkGuards(route *Route, params map[string]string) bool {
for _, guard := range route.Guards {
if !guard(route, params) {
return false
}
}
return true
}
func parseRoute(path string) ([]string, []string) {
var params []string
parts := strings.Split(strings.Trim(path, "/"), "/")
if len(parts) == 1 && parts[0] == "" {
parts = []string{} // Handle root path
}
// Find parameters like :id, :name, etc.
for _, part := range parts {
if strings.HasPrefix(part, ":") {
paramName := part[1:] // Remove the ':'
// Simple validation: param name should start with letter
if len(paramName) > 0 && isLetter(paramName[0]) {
params = append(params, paramName)
}
}
}
return parts, params
}
// isLetter checks if a byte is a letter
func isLetter(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
func defaultNotFound() ui.View {
return ui.VerticalGroup(
ui.TextFromString("404 - Page Not Found").Color("#ff4444"),
ui.TextFromString("The requested page could not be found.").Color("#666"),
)
}