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>
This commit is contained in:
157
navigation/router.go
Normal file
157
navigation/router.go
Normal file
@@ -0,0 +1,157 @@
|
||||
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: ¤tPath,
|
||||
currentView: ¤tView,
|
||||
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"),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user