router API

router

package

API reference for the router package.

S
struct

LoadContext

LoadContext describes the destination passed to a route loader.

router/data.go:27-31
type LoadContext struct

Fields

Name Type Description
Path string
Params map[string]string
Query url.Values
T
type

Loader

Loader resolves data before a route component is committed.

router/data.go:34-34
type Loader func(context.Context, LoadContext) (any, error)
I
interface

RouteDataReceiver

RouteDataReceiver accepts data returned by a route Loader.

router/data.go:37-39
type RouteDataReceiver interface

Methods

SetRouteData
Method

Parameters

any
func SetRouteData(...)
F
function

Status

Status returns the reactive navigation status.

router/data.go:67-69
func Status() *state.Signal[NavigationStatus]

{
	return navigationStatus
}
F
function

Error

Error returns the latest reactive loader error.

router/data.go:72-74
func Error() *state.Signal[error]

{
	return navigationError
}
F
function

Data

Data returns the current route’s reactive loader data.

router/data.go:77-79
func Data() *state.Signal[any]

{
	return currentRouteData
}
F
function

Meta

Meta returns the current route’s reactive metadata.

router/data.go:82-84
func Meta() *state.Signal[map[string]any]

{
	return currentRouteMeta
}
F
function

beginNavigation

Parameters

Returns

router/data.go:86-100
func beginNavigation(parent context.Context) (context.Context, uint64)

{
	if parent == nil {
		parent = context.Background()
	}
	navigationMu.Lock()
	if navigationCancel != nil {
		navigationCancel()
	}
	ctx, cancel := context.WithCancel(parent)
	navigationCancel = cancel
	navigationID++
	id := navigationID
	navigationMu.Unlock()
	return ctx, id
}
F
function

resetNavigation

router/data.go:109-123
func resetNavigation()

{
	navigationMu.Lock()
	if navigationCancel != nil {
		navigationCancel()
	}
	navigationCancel = nil
	navigationID++
	navigationMu.Unlock()
	state.Batch(func() {
		navigationStatus.Set(NavigationIdle)
		navigationError.Set(nil)
		currentRouteData.Set(nil)
		currentRouteMeta.Set(map[string]any{})
	})
}
F
function

commitRouteState

Parameters

data
any
meta
map[string]any
router/data.go:125-132
func commitRouteState(data any, meta map[string]any)

{
	state.Batch(func() {
		currentRouteData.Set(data)
		currentRouteMeta.Set(cloneMeta(meta))
		navigationError.Set(nil)
		navigationStatus.Set(NavigationReady)
	})
}
F
function

failNavigation

Parameters

err
error
router/data.go:134-139
func failNavigation(err error)

{
	state.Batch(func() {
		navigationError.Set(err)
		navigationStatus.Set(NavigationError)
	})
}
F
function

cloneMeta

Parameters

meta
map[string]any

Returns

map[string]any
router/data.go:141-150
func cloneMeta(meta map[string]any) map[string]any

{
	if meta == nil {
		return map[string]any{}
	}
	clone := make(map[string]any, len(meta))
	for key, value := range meta {
		clone[key] = value
	}
	return clone
}
F
function

cloneStringMap

Parameters

values
map[string]string

Returns

map[string]string
router/data.go:152-158
func cloneStringMap(values map[string]string) map[string]string

{
	clone := make(map[string]string, len(values))
	for key, value := range values {
		clone[key] = value
	}
	return clone
}
F
function

URL

URL builds a URL for a named route.

Parameters

name
string
params
map[string]string
query

Returns

string
error
router/data.go:161-183
func URL(name string, params map[string]string, query url.Values) (string, error)

{
	template, ok := namedRoutePath(routes, name)
	if !ok {
		return "", errors.New("router: named route not found")
	}
	segments := strings.Split(template, "/")
	for index, segment := range segments {
		if !strings.HasPrefix(segment, ":") {
			continue
		}
		key := strings.TrimPrefix(segment, ":")
		value, exists := params[key]
		if !exists || value == "" {
			return "", errors.New("router: missing route parameter " + key)
		}
		segments[index] = url.PathEscape(value)
	}
	path := strings.Join(segments, "/")
	if encoded := query.Encode(); encoded != "" {
		path += "?" + encoded
	}
	return path, nil
}
F
function

MustURL

MustURL builds a named URL and panics if it is invalid.

Parameters

name
string
params
map[string]string
query

Returns

string
router/data.go:186-192
func MustURL(name string, params map[string]string, query url.Values) string

{
	path, err := URL(name, params, query)
	if err != nil {
		panic(err)
	}
	return path
}
F
function

namedRoutePath

Parameters

list
name
string

Returns

string
bool
router/data.go:194-204
func namedRoutePath(list []route, name string) (string, bool)

{
	for index := range list {
		if list[index].name == name {
			return list[index].fullPath, true
		}
		if path, ok := namedRoutePath(list[index].children, name); ok {
			return path, true
		}
	}
	return "", false
}
F
function

routeQuery

Parameters

raw
string

Returns

map[string]string
router/data.go:206-220
func routeQuery(raw string) (map[string]string, url.Values)

{
	values, _ := url.ParseQuery(raw)
	params := make(map[string]string, len(values))
	keys := make([]string, 0, len(values))
	for key := range values {
		keys = append(keys, key)
	}
	sort.Strings(keys)
	for _, key := range keys {
		if entries := values[key]; len(entries) > 0 {
			params[key] = entries[0]
		}
	}
	return params, values
}
S
struct

redirectDepthKey

router/data.go:222-222
type redirectDepthKey struct
F
function

nextRedirectContext

Parameters

Returns

router/data.go:224-230
func nextRedirectContext(parent context.Context) (context.Context, error)

{
	depth, _ := parent.Value(redirectDepthKey{}).(int)
	if depth >= 16 {
		return nil, ErrRedirectLoop
	}
	return context.WithValue(parent, redirectDepthKey{}, depth+1), nil
}
F
function

redirectPath

Parameters

template
string
params
map[string]string

Returns

string
error
router/data.go:232-246
func redirectPath(template string, params map[string]string) (string, error)

{
	segments := strings.Split(template, "/")
	for index, segment := range segments {
		if !strings.HasPrefix(segment, ":") {
			continue
		}
		key := strings.TrimPrefix(segment, ":")
		value, ok := params[key]
		if !ok || value == "" {
			return "", errors.New("router: missing redirect parameter " + key)
		}
		segments[index] = url.PathEscape(value)
	}
	return strings.Join(segments, "/"), nil
}
S
struct

testComponent

testComponent implements core.Component and routeParamReceiver for testing.

router/router_query_test.go:12-14
type testComponent struct

Methods

Render
Method

Returns

string
func (*testComponent) Render() string
{ return "" }
Mount
Method
func (*testComponent) Mount()
{}
Unmount
Method
func (*testComponent) Unmount()
{}
OnMount
Method
func (*testComponent) OnMount()
{}
OnUnmount
Method
func (*testComponent) OnUnmount()
{}
GetName
Method

Returns

string
func (*testComponent) GetName() string
{ return "test" }
GetID
Method

Returns

string
func (*testComponent) GetID() string
{ return "" }
SetSlots
Method

Parameters

map[string]any
func (*testComponent) SetSlots(map[string]any)
{}
IsMounted
Method

Returns

bool
func (*testComponent) IsMounted() bool
{ return false }
OnParams
Method

Parameters

p map[string]string
func (*testComponent) OnParams(p map[string]string)
{ c.params = p }

Parameters

p map[string]string
func (*testComponent) SetRouteParams(p map[string]string)
{ c.params = p }

Fields

Name Type Description
params map[string]string
F
function

TestNavigateQueryParams

Parameters

router/router_query_test.go:28-39
func TestNavigateQueryParams(t *testing.T)

{
	Reset()
	RegisterRoute(Route{Path: "/query", Component: func() core.Component { return &testComponent{} }})
	Navigate("/query?key=value")
	tc, ok := currentComponent.(*testComponent)
	if !ok {
		t.Fatalf("expected testComponent, got %T", currentComponent)
	}
	if tc.params["key"] != "value" {
		t.Fatalf("expected query param 'key=value', got %v", tc.params)
	}
}
F
function

TestNavigateNotFound

Parameters

router/router_query_test.go:41-50
func TestNavigateNotFound(t *testing.T)

{
	Reset()
	called := false
	NotFoundCallback = func(string) { called = true }
	Navigate("/missing")
	if !called {
		t.Fatalf("expected NotFoundCallback to be called")
	}
	NotFoundCallback = nil
}
S
struct
Implements: routeParamHandler

routeComponent

router/router_registered_test.go:11-11
type routeComponent struct

Methods

Render
Method

Returns

string
func (routeComponent) Render() string
{ return "" }
Mount
Method
func (routeComponent) Mount()
{}
Unmount
Method
func (routeComponent) Unmount()
{}
OnMount
Method
func (routeComponent) OnMount()
{}
OnUnmount
Method
func (routeComponent) OnUnmount()
{}
GetName
Method

Returns

string
func (routeComponent) GetName() string
{ return "route" }
GetID
Method

Returns

string
func (routeComponent) GetID() string
{ return "route" }
SetSlots
Method

Parameters

map[string]any
func (routeComponent) SetSlots(map[string]any)
{}
IsMounted
Method

Returns

bool
func (routeComponent) IsMounted() bool
{ return false }
OnParams
Method

Parameters

map[string]string
func (routeComponent) OnParams(map[string]string)
{}
F
function

TestRegisteredRoutes

Parameters

router/router_registered_test.go:24-68
func TestRegisteredRoutes(t *testing.T)

{
	Reset()
	RegisterRoute(Route{Path: "/static", Component: func() core.Component { return routeComponent{} }})
	RegisterRoute(Route{
		Path:      "/users",
		Component: func() core.Component { return routeComponent{} },
		Children: []Route{
			{
				Path:      ":id",
				Component: func() core.Component { return routeComponent{} },
			},
			{
				Path:      ":id/profile",
				Component: func() core.Component { return routeComponent{} },
			},
		},
	})

	defs := RegisteredRoutes()
	if len(defs) != 2 {
		t.Fatalf("expected 2 top level routes, got %d", len(defs))
	}
	if defs[0].Path != "/static" || len(defs[0].Params) != 0 {
		t.Fatalf("unexpected static route: %+v", defs[0])
	}

	users := defs[1]
	if users.Path != "/users" {
		t.Fatalf("expected /users path, got %s", users.Path)
	}
	if len(users.Children) != 2 {
		t.Fatalf("expected two children, got %d", len(users.Children))
	}
	child := users.Children[0]
	if child.Path != "/users/:id" {
		t.Fatalf("expected /users/:id full path, got %s", child.Path)
	}
	if len(child.Params) != 1 || child.Params[0] != "id" {
		t.Fatalf("expected id param, got %+v", child.Params)
	}
	profile := users.Children[1]
	if profile.Path != "/users/:id/profile" {
		t.Fatalf("expected /users/:id/profile path, got %s", profile.Path)
	}
}
S
struct
Implements: routeParamHandler

trailingComponent

router/router_trailing_test.go:11-11
type trailingComponent struct

Methods

Render
Method

Returns

string
func (*trailingComponent) Render() string
{ return "" }
Mount
Method
func (*trailingComponent) Mount()
{}
Unmount
Method
func (*trailingComponent) Unmount()
{}
OnMount
Method
func (*trailingComponent) OnMount()
{}
OnUnmount
Method
func (*trailingComponent) OnUnmount()
{}
GetName
Method

Returns

string
func (*trailingComponent) GetName() string
{ return "trailing" }
GetID
Method

Returns

string
func (*trailingComponent) GetID() string
{ return "" }
SetSlots
Method

Parameters

map[string]any
func (*trailingComponent) SetSlots(map[string]any)
{}
IsMounted
Method

Returns

bool
func (*trailingComponent) IsMounted() bool
{ return false }
OnParams
Method

Parameters

map[string]string
func (*trailingComponent) OnParams(map[string]string)
{}
F
function

TestNavigateTrailingSlash

Parameters

router/router_trailing_test.go:24-31
func TestNavigateTrailingSlash(t *testing.T)

{
	Reset()
	RegisterRoute(Route{Path: "/trail", Component: func() core.Component { return &trailingComponent{} }})
	Navigate("/trail/")
	if _, ok := currentComponent.(*trailingComponent); !ok {
		t.Fatalf("expected trailingComponent with trailing slash, got %T", currentComponent)
	}
}
F
function

TestNavigateTrailingSlashNotFound

Parameters

router/router_trailing_test.go:33-43
func TestNavigateTrailingSlashNotFound(t *testing.T)

{
	Reset()
	RegisterRoute(Route{Path: "/trail", Component: func() core.Component { return &trailingComponent{} }})
	called := false
	NotFoundCallback = func(string) { called = true }
	Navigate("/trail/extra")
	if !called {
		t.Fatalf("expected NotFoundCallback for extra path, got none")
	}
	NotFoundCallback = nil
}
S
struct

Outlet

Outlet is a plain component that marks where routed components render.
Include one anywhere in your tree (typically inside an app shell mounted
with MountRoot) and navigation swaps only its subtree: everything around it
keeps its DOM, delegated handlers, and state. With no live outlet the
router falls back to replacing #app wholesale, the pre-outlet behavior.

router/outlet.go:15-17
type Outlet struct

Methods

repaint
Method

repaint re-renders the routed component when its markup is no longer inside the outlet.

func (*Outlet) repaint()
{
	if repainting || currentComponent == nil {
		return
	}
	root := dom.ComponentRoot(o.GetID())
	if root.IsNull() || root.IsUndefined() {
		return
	}
	if child := root.Query("[data-component-id='" + currentComponent.GetID() + "']"); !child.IsNull() && !child.IsUndefined() {
		return
	}
	repainting = true
	defer func() { repainting = false }()
	o.renderChild(currentComponent)
	currentComponent.Mount()
}
OnMount
Method

OnMount registers this outlet as the live navigation target. If a route resolved before the outlet appeared (root mounted after InitRouter), the pending component renders immediately.

func (*Outlet) OnMount()
{
	liveOutlet = o
	o.HTMLComponent.OnMount()
	if currentComponent != nil {
		o.renderChild(currentComponent)
		currentComponent.Mount()
	}
}
OnUnmount
Method

OnUnmount clears the live outlet (the shell around it is going away).

func (*Outlet) OnUnmount()
{
	if liveOutlet == o {
		liveOutlet = nil
	}
	o.HTMLComponent.OnUnmount()
}
renderChild
Method

renderChild replaces the outlet subtree with the routed component's render. Route swaps replace wholesale on purpose: positionally diffing two different component trees leaves stale nodes behind. The marker div stays in place as the anchor, so a re-render of the shell around it can recognise the subtree as the router's and leave it alone.

Parameters

func (*Outlet) renderChild(c core.Component)
{
	root := dom.ComponentRoot(o.GetID())
	if root.IsNull() || root.IsUndefined() {
		dom.UpdateDOM(c.GetID(), core.TryRender(c))
		return
	}
	target := root.Query("[data-router-outlet]")
	if target.IsNull() || target.IsUndefined() {
		target = root
	}
	dom.UpdateDOMIn(target, c.GetID(), core.TryRender(c))
}
F
function

NewOutlet

NewOutlet builds the outlet component; mount it via a dependency include.

Returns

router/outlet.go:24-30
func NewOutlet() *Outlet

{
	c := &Outlet{HTMLComponent: core.NewHTMLComponent("RouterOutlet", outletTpl, nil)}
	c.SetComponent(c)
	c.Init(nil)
	installOutletRepaint()
	return c
}
F
function

installOutletRepaint

installOutletRepaint keeps the routed subtree alive across re-renders of the
shell around it. A persistent root bound to a store re-renders whenever that
store changes, and its fresh markup carries an empty outlet: without this
the routed page would disappear on the first store write after a navigation.
Registered once, when the first outlet is built.

router/outlet.go:42-56
func installOutletRepaint()

{
	if outletRepaintInstalled {
		return
	}
	outletRepaintInstalled = true
	core.OnTemplate(func(componentID, _ string) {
		if liveOutlet == nil || currentComponent == nil {
			return
		}
		if componentID == currentComponent.GetID() {
			return
		}
		liveOutlet.repaint()
	})
}
F
function

MountRoot

MountRoot renders a persistent root component into #app and mounts it. The
root lives outside the navigation lifecycle: the router only ever touches
the outlet inside it. Call it before InitRouter.

Parameters

router/outlet.go:122-127
func MountRoot(c core.Component)

{
	mountedRoot = c
	dom.UpdateDOM(mountedRoot.GetID(), core.TryRender(mountedRoot))
	mountedRoot.Mount()
	core.TriggerMount(mountedRoot)
}
S
struct
Implements: routeParamHandler

canNavigateComponent

router/router_can_navigate_test.go:11-11
type canNavigateComponent struct

Methods

Render
Method

Returns

string
func (*canNavigateComponent) Render() string
{ return "" }
Mount
Method
func (*canNavigateComponent) Mount()
{}
Unmount
Method
func (*canNavigateComponent) Unmount()
{}
OnMount
Method
func (*canNavigateComponent) OnMount()
{}
OnUnmount
Method
func (*canNavigateComponent) OnUnmount()
{}
GetName
Method

Returns

string
func (*canNavigateComponent) GetName() string
{ return "test" }
GetID
Method

Returns

string
func (*canNavigateComponent) GetID() string
{ return "" }
SetSlots
Method

Parameters

map[string]any
func (*canNavigateComponent) SetSlots(map[string]any)
{}
IsMounted
Method

Returns

bool
func (*canNavigateComponent) IsMounted() bool
{ return false }
OnParams
Method

Parameters

map[string]string
func (*canNavigateComponent) OnParams(map[string]string)
{}
F
function

TestCanNavigate

Parameters

router/router_can_navigate_test.go:24-36
func TestCanNavigate(t *testing.T)

{
	Reset()
	RegisterRoute(Route{Path: "/can", Component: func() core.Component { return &canNavigateComponent{} }})
	if !CanNavigate("/can") {
		t.Fatalf("expected true for registered route")
	}
	if !CanNavigate("/can?foo=bar") {
		t.Fatalf("expected true for registered route with query")
	}
	if CanNavigate("/missing") {
		t.Fatalf("expected false for unregistered route")
	}
}
F
function

TestReplaceKeepsHistoryLength

Parameters

router/router_history_test.go:12-36
func TestReplaceKeepsHistoryLength(t *testing.T)

{
	Reset()
	originalPath := js.Location().Get("pathname").String() +
		js.Location().Get("search").String() +
		js.Location().Get("hash").String()
	t.Cleanup(func() {
		js.History().Call("replaceState", nil, "", originalPath)
		Reset()
	})

	RegisterRoute(Route{
		Path:      "/replace-history",
		Component: func() core.Component { return routeComponent{} },
	})
	before := js.History().Get("length").Int()

	Replace("/replace-history")

	if got := js.Location().Get("pathname").String(); got != "/replace-history" {
		t.Fatalf("expected replaced path, got %q", got)
	}
	if got := js.History().Get("length").Int(); got != before {
		t.Fatalf("history length changed from %d to %d", before, got)
	}
}
F
function

TestHistoryNoneGuardFallbackDoesNotPush

Parameters

router/router_history_test.go:38-57
func TestHistoryNoneGuardFallbackDoesNotPush(t *testing.T)

{
	Reset()
	t.Cleanup(Reset)
	RegisterRoute(Route{
		Path:      "/",
		Component: func() core.Component { return routeComponent{} },
	})
	RegisterRoute(Route{
		Path:      "/protected",
		Component: func() core.Component { return routeComponent{} },
		Guards:    []Guard{func(map[string]string) bool { return false }},
	})
	before := js.History().Get("length").Int()

	navigate("/protected", historyNone)

	if got := js.History().Get("length").Int(); got != before {
		t.Fatalf("history length changed from %d to %d", before, got)
	}
}
F
function

TestShellRerenderKeepsPageBuiltDOM

A page that fills part of itself after mount (a card injected with SetHTML,
the common pattern for markup built from fetched data) must keep that DOM
when the shell around the outlet re-renders.

Parameters

router/outlet_preserve_test.go:18-64
func TestShellRerenderKeepsPageBuiltDOM(t *testing.T)

{
	Reset()
	if dom.ByID("app").IsNull() {
		host := dom.CreateElement("div")
		host.SetAttr("id", "app")
		dom.Doc().Body().AppendChild(host)
	}

	shellStore := state.NewStore("shellpreserve", state.WithModule("app"))
	shellStore.Set("nav", []any{map[string]any{"label": "one"}})
	defer state.GlobalStoreManager.UnregisterStore("app", "shellpreserve")

	shell := core.NewHTMLComponent("PreserveShell", []byte(`<root>
@for:it in store:app.shellpreserve.nav
<span class="nav">@prop:it.label</span>
@endfor
@include:outlet
</root>`), nil)
	shell.SetComponent(shell)
	shell.AddDependency("outlet", NewOutlet())
	shell.Init(nil)

	page := core.NewHTMLComponent("PreservePage", []byte(`<root><div data-card></div></root>`), nil)
	page.SetComponent(page)
	page.Init(nil)
	Page("/preserve-test", page)

	MountRoot(shell)
	Navigate("/preserve-test")

	// the page fills its card after mount, the way a fetch callback would
	dom.Query("[data-card]").SetHTML(`<b id="page-built">built</b>`)
	if !strings.Contains(dom.ByID("app").HTML(), "page-built") {
		t.Fatal("card was not injected")
	}

	shellStore.Set("nav", []any{map[string]any{"label": "one"}, map[string]any{"label": "two"}})
	waitForRouterRender()

	html := dom.ByID("app").HTML()
	if !strings.Contains(html, "two") {
		t.Fatalf("shell did not re-render: %s", html)
	}
	if !strings.Contains(html, "page-built") {
		t.Fatalf("shell re-render wiped the DOM the page had built: %s", html)
	}
}
F
function

waitForRouterRender

router/outlet_preserve_test.go:66-68
func waitForRouterRender()

{
	time.Sleep(20 * time.Millisecond)
}
F
function

TestOutletSurvivesShellRerender

A shell bound to a store re-renders on every write, and its fresh markup
carries an empty outlet. The routed page has to survive that.

Parameters

router/outlet_repaint_test.go:16-63
func TestOutletSurvivesShellRerender(t *testing.T)

{
	Reset()
	if dom.ByID("app").IsNull() {
		host := dom.CreateElement("div")
		host.SetAttr("id", "app")
		dom.Doc().Body().AppendChild(host)
	}

	shellStore := state.NewStore("shellrepaint", state.WithModule("app"))
	shellStore.Set("title", "first")
	shellStore.Set("nav", []any{map[string]any{"label": "one"}})
	defer state.GlobalStoreManager.UnregisterStore("app", "shellrepaint")

	// a @for over a store list is the shape that re-renders the whole shell
	shell := core.NewHTMLComponent("RepaintShell", []byte(`<root>
<header>@store:app.shellrepaint.title</header>
@for:it in store:app.shellrepaint.nav
<span class="nav">@prop:it.label</span>
@endfor
@include:outlet
</root>`), nil)
	shell.SetComponent(shell)
	shell.AddDependency("outlet", NewOutlet())
	shell.Init(nil)

	page := core.NewHTMLComponent("RepaintPage", []byte(`<root><main id="routed-page">page</main></root>`), nil)
	page.SetComponent(page)
	page.Init(nil)
	Page("/repaint-test", page)

	MountRoot(shell)
	Navigate("/repaint-test")

	if html := dom.ByID("app").HTML(); !strings.Contains(html, "routed-page") {
		t.Fatalf("page did not render into the outlet: %s", html)
	}

	shellStore.Set("nav", []any{map[string]any{"label": "one"}, map[string]any{"label": "two"}})
	waitForRouterRender()

	html := dom.ByID("app").HTML()
	if !strings.Contains(html, "two") {
		t.Fatalf("shell did not re-render: %s", html)
	}
	if !strings.Contains(html, "routed-page") {
		t.Fatalf("shell re-render dropped the routed page: %s", html)
	}
}
T
type

Guard

Guard is a function that determines whether navigation to a route is
permitted based on the provided parameters.

router/router.go:24-24
type Guard func(map[string]string) bool
S
struct

Route

Route describes a routing rule that maps a path to a component and optional
guards or child routes.

Component accepts three forms:
- A *types.View instance: reused every navigation (singleton).
- A func() *types.View: called each navigation to create a fresh instance.
- A func() core.Component: called each navigation (legacy).

router/router.go:33-42
type Route struct

Fields

Name Type Description
Path string
Name string
Component any
Guards []Guard
Children []Route
Loader Loader
Redirect string
Meta map[string]any
F
function

Singleton

Singleton wraps a pre-created View into a Route.Component value.
Every navigation returns the same instance, no re-creation.

Parameters

Returns

any
router/router.go:46-48
func Singleton(v *types.View) any

{
	return v
}
S
struct

route

router/router.go:50-65
type route struct

Fields

Name Type Description
pattern string
fullPath string
name string
regex *regexp.Regexp
paramNames []string
matchNames []string
component core.Component
loader func() core.Component
singleton bool
children []route
guards []Guard
dataLoader Loader
redirect string
meta map[string]any
S
struct

RegisteredRoute

RegisteredRoute describes a registered route in a navigable tree form.

router/router.go:68-83
type RegisteredRoute struct

Fields

Name Type Description
Template string json:"template"
Path string json:"path"
Name string json:"name,omitempty"
Params []string json:"params"
Children []RegisteredRoute json:"children"
Meta map[string]any json:"meta,omitempty"
F
function

Reset

Reset clears the router’s registered routes and current component.
It is primarily intended for use in tests to ensure a clean state.

router/router.go:105-113
func Reset()

{
	routes = nil
	currentComponent = nil
	NotFoundComponent = nil
	NotFoundCallback = nil
	activePathSig.Set("/")
	resetNavigation()
	scrollPositions = map[string][2]int{}
}
F
function

RegisterRoute

RegisterRoute adds a new Route to the router’s configuration.

Parameters

r
router/router.go:116-118
func RegisterRoute(r Route)

{
	routes = append(routes, buildRoute(r))
}
F
function

buildRoute

Parameters

r

Returns

router/router.go:120-122
func buildRoute(r Route) route

{
	return buildRouteAt(r, "")
}
F
function

buildRouteAt

Parameters

r
parent
string

Returns

router/router.go:124-196
func buildRouteAt(r Route, parent string) route

{
	fullPath := resolveRoutePath(parent, r.Path)
	segments := strings.Split(strings.Trim(fullPath, "/"), "/")
	regexParts := make([]string, len(segments))
	matchNames := []string{}

	for i, segment := range segments {
		if strings.HasPrefix(segment, ":") {
			name := strings.TrimPrefix(segment, ":")
			matchNames = append(matchNames, name)
			regexParts[i] = "([^/]+)"
		} else {
			regexParts[i] = regexp.QuoteMeta(segment)
		}
	}

	paramNames := []string{}
	for _, segment := range strings.Split(strings.Trim(r.Path, "/"), "/") {
		if strings.HasPrefix(segment, ":") {
			paramNames = append(paramNames, strings.TrimPrefix(segment, ":"))
		}
	}

	pathRegex := strings.Join(regexParts, "/")
	suffix := "/?$"
	if len(r.Children) > 0 {
		suffix = "(?:/|$)"
	}
	if pathRegex == "" {
		if len(r.Children) > 0 {
			suffix = ""
		} else {
			suffix = "$"
		}
	}
	pattern := "^/" + pathRegex + suffix
	var loader func() core.Component
	var singleton bool
	switch c := r.Component.(type) {
	case *types.View:
		comp := c
		loader = func() core.Component { return comp }
		singleton = true
	case func() *types.View:
		loader = func() core.Component { return c() }
	case func() core.Component:
		loader = c
	case core.Component:
		comp := c
		loader = func() core.Component { return comp }
		singleton = true
	}
	rt := route{
		pattern:    r.Path,
		fullPath:   fullPath,
		name:       r.Name,
		regex:      regexp.MustCompile(pattern),
		paramNames: paramNames,
		matchNames: matchNames,
		loader:     loader,
		singleton:  singleton,
		guards:     r.Guards,
		dataLoader: r.Loader,
		redirect:   r.Redirect,
		meta:       cloneMeta(r.Meta),
	}

	for _, child := range r.Children {
		rt.children = append(rt.children, buildRouteAt(child, fullPath))
	}

	return rt
}
F
function

RegisteredRoutes

RegisteredRoutes returns the registered routes including nested children and
resolved full paths. The data can be used for tooling and diagnostics.

Returns

router/router.go:200-206
func RegisteredRoutes() []RegisteredRoute

{
	out := make([]RegisteredRoute, 0, len(routes))
	for i := range routes {
		out = append(out, snapshotRoute(&routes[i], ""))
	}
	return out
}
F
function

snapshotRoute

Parameters

r
parent
string

Returns

router/router.go:208-224
func snapshotRoute(r *route, parent string) RegisteredRoute

{
	params := make([]string, len(r.paramNames))
	copy(params, r.paramNames)
	full := resolveRoutePath(parent, r.pattern)
	children := make([]RegisteredRoute, len(r.children))
	for i := range r.children {
		children[i] = snapshotRoute(&r.children[i], full)
	}
	return RegisteredRoute{
		Template: r.pattern,
		Path:     full,
		Name:     r.name,
		Params:   params,
		Children: children,
		Meta:     cloneMeta(r.meta),
	}
}
F
function

resolveRoutePath

Parameters

parent
string
path
string

Returns

string
router/router.go:226-244
func resolveRoutePath(parent, path string) string

{
	if path == "" {
		if parent == "" {
			return "/"
		}
		return parent
	}
	if strings.HasPrefix(path, "/") {
		return path
	}
	trimmed := strings.TrimPrefix(path, "/")
	if parent == "" || parent == "/" {
		return "/" + trimmed
	}
	if strings.HasSuffix(parent, "/") {
		return parent + trimmed
	}
	return parent + "/" + trimmed
}
I
interface

routeParamReceiver

router/router.go:246-248
type routeParamReceiver interface

Methods

Parameters

map[string]string
func SetRouteParams(...)
F
function

matchRoute

Parameters

routes
path
string

Returns

map[string]string
router/router.go:250-275
func matchRoute(routes []route, path string) (*route, []Guard, map[string]string)

{
	for i := range routes {
		r := &routes[i]
		matches := r.regex.FindStringSubmatch(path)
		if matches == nil {
			if child, guards, params := matchRoute(r.children, path); child != nil {
				return child, append(r.guards, guards...), params
			}
			continue
		}
		params := map[string]string{}
		for i, name := range r.matchNames {
			if i+1 < len(matches) {
				params[name] = decodeRouteParam(matches[i+1])
			}
		}
		if child, guards, childParams := matchRoute(r.children, path); child != nil {
			return child, append(r.guards, guards...), childParams
		}
		matchedPath := strings.TrimSuffix(matches[0], "/")
		if (r.loader != nil || r.redirect != "") && matchedPath == strings.TrimSuffix(path, "/") {
			return r, r.guards, params
		}
	}
	return nil, nil, nil
}
F
function

decodeRouteParam

Parameters

value
string

Returns

string
router/router.go:277-283
func decodeRouteParam(value string) string

{
	decoded, err := url.PathUnescape(value)
	if err != nil {
		return value
	}
	return decoded
}
T
type

historyMode

router/router.go:285-285
type historyMode uint8
F
function

Replace

Replace navigates without adding a new browser history entry.

Parameters

fullPath
string
router/router.go:301-303
func Replace(fullPath string)

{
	navigate(fullPath, historyReplace)
}
F
function

updateHistory

Parameters

path
string
router/router.go:487-494
func updateHistory(mode historyMode, path string)

{
	switch mode {
	case historyPush:
		js.History().Call("pushState", nil, "", path)
	case historyReplace:
		js.History().Call("replaceState", nil, "", path)
	}
}
F
function

SetScrollRestoration

SetScrollRestoration enables or disables router-managed scroll positions.

Parameters

enabled
bool
router/router.go:497-499
func SetScrollRestoration(enabled bool)

{
	scrollEnabled = enabled
}
F
function

saveScroll

router/router.go:501-510
func saveScroll()

{
	if !scrollEnabled || currentComponent == nil {
		return
	}
	path := activePathSig.Get()
	scrollPositions[path] = [2]int{
		js.Window().Get("scrollX").Int(),
		js.Window().Get("scrollY").Int(),
	}
}
F
function

restoreScroll

Parameters

path
string
history
meta
map[string]any
router/router.go:512-524
func restoreScroll(path string, history historyMode, meta map[string]any)

{
	if !scrollEnabled {
		return
	}
	if preserve, _ := meta["preserveScroll"].(bool); preserve {
		return
	}
	position := [2]int{}
	if history == historyNone {
		position = scrollPositions[path]
	}
	js.Window().Call("scrollTo", position[0], position[1])
}
F
function

CanNavigate

CanNavigate reports whether the specified path matches a registered route.

Parameters

fullPath
string

Returns

bool
router/router.go:527-534
func CanNavigate(fullPath string) bool

{
	path := fullPath
	if idx := strings.Index(fullPath, "?"); idx != -1 {
		path = fullPath[:idx]
	}
	r, _, _ := matchRoute(routes, path)
	return r != nil
}
F
function

ExposeNavigate

ExposeNavigate makes the Navigate function accessible from JavaScript and
automatically routes internal anchor clicks.

router/router.go:538-564
func ExposeNavigate()

{
	exposeNavigateOnce.Do(func() {
		js.ExposeFunc("goNavigate", func(_ js.Value, args []js.Value) any {
			path := args[0].String()
			Navigate(path)
			return nil
		})

		events.On("click", js.Document(), func(evt js.Value) {
			link := evt.Get("target").Call("closest", "a[href]")
			if !link.Truthy() {
				return
			}
			if t := link.Get("target").String(); t != "" && t != "_self" {
				return
			}
			if link.Get("origin").String() != js.Location().Get("origin").String() {
				return
			}
			path := link.Get("pathname").String() + link.Get("search").String()
			if CanNavigate(path) {
				evt.Call("preventDefault")
				Navigate(path)
			}
		})
	})
}
F
function

Page

Page registers a route with path, component and optional guards.

Parameters

path
string
component
any
guards
...Guard
router/router.go:567-573
func Page(path string, component any, guards ...Guard)

{
	RegisterRoute(Route{
		Path:      path,
		Component: component,
		Guards:    guards,
	})
}
F
function

Group

Group creates nested routes under a common path prefix
and registers them. Returns the parent Route for chaining.

Parameters

prefix
string
fn
func(*GroupBuilder)
router/router.go:577-584
func Group(prefix string, fn func(*GroupBuilder))

{
	b := &GroupBuilder{prefix: prefix}
	fn(b)
	RegisterRoute(Route{
		Path:     prefix,
		Children: b.children,
	})
}
S
struct

GroupBuilder

GroupBuilder collects child routes within a Group callback.

router/router.go:587-590
type GroupBuilder struct

Methods

Page
Method

Page adds a child route within a Group.

Parameters

path string
component any
guards ...Guard
func (*GroupBuilder) Page(path string, component any, guards ...Guard)
{
	g.children = append(g.children, Route{
		Path:      path,
		Component: component,
		Guards:    guards,
	})
}
Page
Method

Page adds a route to the group.

Parameters

path string
component any
guards ...Guard
func (*GroupBuilder) Page(path string, component any, guards ...Guard)
{
	g.children = append(g.children, Route{
		Path:      path,
		Component: component,
		Guards:    guards,
	})
}

Fields

Name Type Description
prefix string
children []Route
F
function

InitRouter

InitRouter initializes the router and begins listening for navigation
events.

router/router.go:603-618
func InitRouter()

{
	ExposeNavigate()

	// The popstate listener lives for the whole app lifetime; the stop
	// function is intentionally discarded.
	ch, _ := events.Listen("popstate", js.Window())
	go func() {
		for range ch {
			path := js.Location().Get("pathname").String() + js.Location().Get("search").String()
			core.TryNavigate(path, func() { navigate(path, historyNone) })
		}
	}()

	currentPath := js.Location().Get("pathname").String() + js.Location().Get("search").String()
	navigate(currentPath, historyNone)
}
F
function

SetNavItems

SetNavItems registers the navigation items to be consumed by templates.

Parameters

items
router/router.go:628-630
func SetNavItems(items []NavItem)

{
	navItems = items
}
F
function

RouterData

RouterData returns the values exposed to component templates.

Returns

map[string]any
router/router.go:652-657
func RouterData() map[string]any

{
	return map[string]any{
		"ActivePath": activePathSig,
		"NavItems":   NavItemsMap(),
	}
}
F
function

TemplateData

TemplateData returns the values exposed to component templates.

Returns

map[string]any
router/router.go:660-660
func TemplateData() map[string]any

{ return RouterData() }
F
function

ActivePath

ActivePath returns the reactive signal holding the current route path.

router/router.go:663-665
func ActivePath() *state.Signal[string]

{
	return activePathSig
}
F
function

TestSameInstanceRouteUpdatesInPlace

Registering one component instance for a base path and its “:id” variant must
update it in place across navigation (and browser back/forward) instead of
unmounting and remounting an unchanged view. A remount rebuilds the
component’s DOM and wipes anything it injected after mount (e.g. a detail
panel a fetch callback filled in), so the injected marker surviving the
navigation is the proof it stayed mounted.

Parameters

router/router_inplace_test.go:19-66
func TestSameInstanceRouteUpdatesInPlace(t *testing.T)

{
	Reset()
	if dom.ByID("app").IsNull() {
		host := dom.CreateElement("div")
		host.SetAttr("id", "app")
		dom.Doc().Body().AppendChild(host)
	}

	shell := core.NewHTMLComponent("InplaceShell", []byte(`<root>@include:outlet</root>`), nil)
	shell.SetComponent(shell)
	shell.AddDependency("outlet", NewOutlet())
	shell.Init(nil)

	page := core.NewHTMLComponent("InplacePage", []byte(`<root><div data-inplace></div></root>`), nil)
	page.SetComponent(page)
	page.Init(nil)

	// The same instance backs both the base path and its :id variant.
	Page("/inplace", page)
	Page("/inplace/:id", page)

	MountRoot(shell)

	Navigate("/inplace")
	// The page fills itself after mount, the way a fetch callback would.
	dom.Query("[data-inplace]").SetHTML(`<b id="inplace-built">built</b>`)
	if !strings.Contains(dom.ByID("app").HTML(), "inplace-built") {
		t.Fatal("marker was not injected")
	}

	// Navigate to the :id variant of the same instance.
	Navigate("/inplace/42")
	if got := ActivePath().Get(); got != "/inplace/42" {
		t.Fatalf("active path not updated: %s", got)
	}
	if !strings.Contains(dom.ByID("app").HTML(), "inplace-built") {
		t.Fatalf("navigating to the same-instance :id route remounted the view (marker wiped): %s", dom.ByID("app").HTML())
	}

	// Back to the base path stays in place too.
	Navigate("/inplace")
	if got := ActivePath().Get(); got != "/inplace" {
		t.Fatalf("active path not restored: %s", got)
	}
	if !strings.Contains(dom.ByID("app").HTML(), "inplace-built") {
		t.Fatalf("navigating back remounted the view (marker wiped): %s", dom.ByID("app").HTML())
	}
}
T
type

Guard

Guard decides whether a route can be entered.

router/router_stub.go:17-17
type Guard func(map[string]string) bool
S
struct

Route

Route describes a route and its optional children, loader, and metadata.

router/router_stub.go:20-29
type Route struct

Fields

Name Type Description
Path string
Name string
Component any
Guards []Guard
Children []Route
Loader Loader
Redirect string
Meta map[string]any
F
function

Singleton

Singleton marks a view instance for reuse between navigations.

Parameters

Returns

any
router/router_stub.go:32-34
func Singleton(v *types.View) any

{
	return v
}
S
struct

route

router/router_stub.go:36-51
type route struct

Fields

Name Type Description
pattern string
fullPath string
name string
regex *regexp.Regexp
paramNames []string
matchNames []string
component core.Component
loader func() core.Component
singleton bool
children []route
guards []Guard
dataLoader Loader
redirect string
meta map[string]any
S
struct

RegisteredRoute

RegisteredRoute is a serializable snapshot of a route.

router/router_stub.go:54-61
type RegisteredRoute struct

Fields

Name Type Description
Template string json:"template"
Path string json:"path"
Name string json:"name,omitempty"
Params []string json:"params"
Children []RegisteredRoute json:"children"
Meta map[string]any json:"meta,omitempty"
F
function

Reset

Reset clears registered routes and navigation state.

router/router_stub.go:77-84
func Reset()

{
	routes = nil
	currentComponent = nil
	NotFoundComponent = nil
	NotFoundCallback = nil
	activePathSig.Set("/")
	resetNavigation()
}
F
function

RegisterRoute

RegisterRoute adds a route to the router.

Parameters

r
router/router_stub.go:87-89
func RegisterRoute(r Route)

{
	routes = append(routes, buildRoute(r))
}
F
function

buildRoute

Parameters

r

Returns

router/router_stub.go:91-93
func buildRoute(r Route) route

{
	return buildRouteAt(r, "")
}
F
function

buildRouteAt

Parameters

r
parent
string

Returns

router/router_stub.go:95-168
func buildRouteAt(r Route, parent string) route

{
	fullPath := resolveRoutePath(parent, r.Path)
	segments := strings.Split(strings.Trim(fullPath, "/"), "/")
	regexParts := make([]string, len(segments))
	matchNames := []string{}

	for i, segment := range segments {
		if strings.HasPrefix(segment, ":") {
			name := strings.TrimPrefix(segment, ":")
			matchNames = append(matchNames, name)
			regexParts[i] = "([^/]+)"
		} else {
			regexParts[i] = regexp.QuoteMeta(segment)
		}
	}

	paramNames := []string{}
	for _, segment := range strings.Split(strings.Trim(r.Path, "/"), "/") {
		if strings.HasPrefix(segment, ":") {
			paramNames = append(paramNames, strings.TrimPrefix(segment, ":"))
		}
	}

	pathRegex := strings.Join(regexParts, "/")
	suffix := "/?$"
	if len(r.Children) > 0 {
		suffix = "(?:/|$)"
	}
	if pathRegex == "" {
		if len(r.Children) > 0 {
			suffix = ""
		} else {
			suffix = "$"
		}
	}
	pattern := "^/" + pathRegex + suffix

	var loader func() core.Component
	var singleton bool
	switch c := r.Component.(type) {
	case *types.View:
		comp := c
		loader = func() core.Component { return comp }
		singleton = true
	case func() *types.View:
		loader = func() core.Component { return c() }
	case func() core.Component:
		loader = c
	case core.Component:
		comp := c
		loader = func() core.Component { return comp }
		singleton = true
	}
	rt := route{
		pattern:    r.Path,
		fullPath:   fullPath,
		name:       r.Name,
		regex:      regexp.MustCompile(pattern),
		paramNames: paramNames,
		matchNames: matchNames,
		loader:     loader,
		singleton:  singleton,
		guards:     r.Guards,
		dataLoader: r.Loader,
		redirect:   r.Redirect,
		meta:       cloneMeta(r.Meta),
	}

	for _, child := range r.Children {
		rt.children = append(rt.children, buildRouteAt(child, fullPath))
	}

	return rt
}
F
function

RegisteredRoutes

RegisteredRoutes returns snapshots of all registered routes.

Returns

router/router_stub.go:171-177
func RegisteredRoutes() []RegisteredRoute

{
	out := make([]RegisteredRoute, 0, len(routes))
	for i := range routes {
		out = append(out, snapshotRoute(&routes[i], ""))
	}
	return out
}
F
function

snapshotRoute

Parameters

r
parent
string

Returns

router/router_stub.go:179-195
func snapshotRoute(r *route, parent string) RegisteredRoute

{
	params := make([]string, len(r.paramNames))
	copy(params, r.paramNames)
	full := resolveRoutePath(parent, r.pattern)
	children := make([]RegisteredRoute, len(r.children))
	for i := range r.children {
		children[i] = snapshotRoute(&r.children[i], full)
	}
	return RegisteredRoute{
		Template: r.pattern,
		Path:     full,
		Name:     r.name,
		Params:   params,
		Children: children,
		Meta:     cloneMeta(r.meta),
	}
}
F
function

resolveRoutePath

Parameters

parent
string
path
string

Returns

string
router/router_stub.go:197-215
func resolveRoutePath(parent, path string) string

{
	if path == "" {
		if parent == "" {
			return "/"
		}
		return parent
	}
	if strings.HasPrefix(path, "/") {
		return path
	}
	trimmed := strings.TrimPrefix(path, "/")
	if parent == "" || parent == "/" {
		return "/" + trimmed
	}
	if strings.HasSuffix(parent, "/") {
		return parent + trimmed
	}
	return parent + "/" + trimmed
}
I
interface

routeParamReceiver

router/router_stub.go:217-219
type routeParamReceiver interface

Methods

Parameters

map[string]string
func SetRouteParams(...)
I
interface

routeParamHandler

router/router_stub.go:221-223
type routeParamHandler interface

Methods

OnParams
Method

Parameters

map[string]string
func OnParams(...)
F
function

matchRoute

Parameters

routes
path
string

Returns

map[string]string
router/router_stub.go:225-250
func matchRoute(routes []route, path string) (*route, []Guard, map[string]string)

{
	for i := range routes {
		r := &routes[i]
		matches := r.regex.FindStringSubmatch(path)
		if matches == nil {
			if child, guards, params := matchRoute(r.children, path); child != nil {
				return child, append(r.guards, guards...), params
			}
			continue
		}
		params := map[string]string{}
		for i, name := range r.matchNames {
			if i+1 < len(matches) {
				params[name] = decodeRouteParam(matches[i+1])
			}
		}
		if child, guards, childParams := matchRoute(r.children, path); child != nil {
			return child, append(r.guards, guards...), childParams
		}
		matchedPath := strings.TrimSuffix(matches[0], "/")
		if (r.loader != nil || r.redirect != "") && matchedPath == strings.TrimSuffix(path, "/") {
			return r, r.guards, params
		}
	}
	return nil, nil, nil
}
F
function

decodeRouteParam

Parameters

value
string

Returns

string
router/router_stub.go:252-258
func decodeRouteParam(value string) string

{
	decoded, err := url.PathUnescape(value)
	if err != nil {
		return value
	}
	return decoded
}
F
function

Replace

Replace behaves like Navigate outside browser builds.

Parameters

fullPath
string
router/router_stub.go:380-382
func Replace(fullPath string)

{
	Navigate(fullPath)
}
F
function

SetScrollRestoration

SetScrollRestoration is a no-op outside browser builds.

Parameters

bool
router/router_stub.go:385-385
func SetScrollRestoration(bool)

{}
F
function

CanNavigate

CanNavigate reports whether a path matches a registered route.

Parameters

fullPath
string

Returns

bool
router/router_stub.go:388-395
func CanNavigate(fullPath string) bool

{
	path := fullPath
	if idx := strings.Index(fullPath, "?"); idx != -1 {
		path = fullPath[:idx]
	}
	r, _, _ := matchRoute(routes, path)
	return r != nil
}
F
function

Page

Page registers a route with an optional set of guards.

Parameters

path
string
component
any
guards
...Guard
router/router_stub.go:398-404
func Page(path string, component any, guards ...Guard)

{
	RegisterRoute(Route{
		Path:      path,
		Component: component,
		Guards:    guards,
	})
}
F
function

Group

Group registers a set of child routes below a path prefix.

Parameters

prefix
string
fn
func(*GroupBuilder)
router/router_stub.go:407-414
func Group(prefix string, fn func(*GroupBuilder))

{
	b := &GroupBuilder{prefix: prefix}
	fn(b)
	RegisterRoute(Route{
		Path:     prefix,
		Children: b.children,
	})
}
S
struct

GroupBuilder

GroupBuilder collects routes for Group.

router/router_stub.go:417-420
type GroupBuilder struct

Methods

Page
Method

Page adds a child route within a Group.

Parameters

path string
component any
guards ...Guard
func (*GroupBuilder) Page(path string, component any, guards ...Guard)
{
	g.children = append(g.children, Route{
		Path:      path,
		Component: component,
		Guards:    guards,
	})
}
Page
Method

Page adds a route to the group.

Parameters

path string
component any
guards ...Guard
func (*GroupBuilder) Page(path string, component any, guards ...Guard)
{
	g.children = append(g.children, Route{
		Path:      path,
		Component: component,
		Guards:    guards,
	})
}

Fields

Name Type Description
prefix string
children []Route
F
function

ExposeNavigate

ExposeNavigate is a no-op outside browser builds.

router/router_stub.go:432-432
func ExposeNavigate()

{}
F
function

InitRouter

InitRouter is a no-op outside browser builds.

router/router_stub.go:435-435
func InitRouter()

{}
F
function

SetNavItems

SetNavItems registers the navigation items.

Parameters

items
router/router_stub.go:445-447
func SetNavItems(items []NavItem)

{
	navItems = items
}
F
function

RouterData

RouterData returns the values exposed to component templates.

Returns

map[string]any
router/router_stub.go:468-473
func RouterData() map[string]any

{
	return map[string]any{
		"ActivePath": activePathSig,
		"NavItems":   NavItemsMap(),
	}
}
F
function

TemplateData

TemplateData returns the values exposed to component templates.

Returns

map[string]any
router/router_stub.go:476-476
func TemplateData() map[string]any

{ return RouterData() }
F
function

ActivePath

ActivePath returns the reactive signal holding the current route path.

router/router_stub.go:479-481
func ActivePath() *state.Signal[string]

{
	return activePathSig
}
S
struct

recordComponent

router/router_test.go:10-13
type recordComponent struct

Methods

Render
Method

Returns

string
func (*recordComponent) Render() string
{ return "" }
Mount
Method
func (*recordComponent) Mount()
{}
Unmount
Method
func (*recordComponent) Unmount()
{}
OnMount
Method
func (*recordComponent) OnMount()
{}
OnUnmount
Method
func (*recordComponent) OnUnmount()
{}
GetName
Method

Returns

string
func (*recordComponent) GetName() string
{ return c.name }
GetID
Method

Returns

string
func (*recordComponent) GetID() string
{ return c.name }
SetSlots
Method

Parameters

map[string]any
func (*recordComponent) SetSlots(map[string]any)
{}
IsMounted
Method

Returns

bool
func (*recordComponent) IsMounted() bool
{ return false }
OnParams
Method

Parameters

p map[string]string
func (*recordComponent) OnParams(p map[string]string)
{
	c.SetRouteParams(p)
}

Parameters

p map[string]string
func (*recordComponent) SetRouteParams(p map[string]string)
{
	c.params = map[string]string{}
	for k, v := range p {
		c.params[k] = v
	}
}

Fields

Name Type Description
name string
params map[string]string
F
function

resetRouter

Parameters

router/router_test.go:34-38
func resetRouter(t *testing.T)

{
	t.Helper()
	Reset()
	t.Cleanup(func() { Reset() })
}
F
function

mustRecord

Parameters

Returns

router/router_test.go:40-47
func mustRecord(t *testing.T, c core.Component) *recordComponent

{
	t.Helper()
	rc, ok := c.(*recordComponent)
	if !ok {
		t.Fatalf("expected *recordComponent, got %T", c)
	}
	return rc
}
F
function

TestRegisterRoute_BasicRouting

Parameters

router/router_test.go:49-72
func TestRegisterRoute_BasicRouting(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{Path: "/a", Component: func() core.Component { return &recordComponent{name: "a"} }})
	RegisterRoute(Route{Path: "/users/:id", Component: func() core.Component { return &recordComponent{name: "user"} }})

	NavigateTo("/a")
	if got := mustRecord(t, CurrentComponent()).name; got != "a" {
		t.Fatalf("expected current component 'a', got %q", got)
	}

	NavigateTo("/users/123")
	rc := mustRecord(t, CurrentComponent())
	if rc.params["id"] != "123" {
		t.Fatalf("expected id=123, got %v", rc.params)
	}

	var gotPath string
	NotFoundCallback = func(p string) { gotPath = p }
	NavigateTo("/missing")
	if gotPath != "/missing" {
		t.Fatalf("expected NotFoundCallback '/missing', got %q", gotPath)
	}
}
F
function

TestNavigateTo_CurrentComponent

Parameters

router/router_test.go:74-87
func TestNavigateTo_CurrentComponent(t *testing.T)

{
	resetRouter(t)

	NavigateTo("/nothing")
	if CurrentComponent() != nil {
		t.Fatalf("expected nil current component before routes")
	}

	RegisterRoute(Route{Path: "/home", Component: func() core.Component { return &recordComponent{name: "home"} }})
	NavigateTo("/home")
	if got := mustRecord(t, CurrentComponent()).name; got != "home" {
		t.Fatalf("expected 'home', got %q", got)
	}
}
F
function

TestRouteGuards_BlockNavigation

Parameters

router/router_test.go:89-114
func TestRouteGuards_BlockNavigation(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{Path: "/", Component: func() core.Component { return &recordComponent{name: "root"} }})

	var guardParams map[string]string
	RegisterRoute(Route{
		Path:      "/admin/:id",
		Component: func() core.Component { return &recordComponent{name: "admin"} },
		Guards: []Guard{func(p map[string]string) bool {
			guardParams = map[string]string{}
			for k, v := range p {
				guardParams[k] = v
			}
			return false
		}},
	})

	NavigateTo("/admin/42")
	if guardParams["id"] != "42" {
		t.Fatalf("expected guard to receive id=42, got %v", guardParams)
	}
	if got := mustRecord(t, CurrentComponent()).name; got != "root" {
		t.Fatalf("expected navigation to '/', got %q", got)
	}
}
F
function

TestQueryParams

Parameters

router/router_test.go:116-127
func TestQueryParams(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{Path: "/search/:kind", Component: func() core.Component { return &recordComponent{name: "search"} }})
	NavigateTo("/search/books?q=go&lang=en")

	rc := mustRecord(t, CurrentComponent())
	want := map[string]string{"kind": "books", "q": "go", "lang": "en"}
	if !reflect.DeepEqual(rc.params, want) {
		t.Fatalf("expected params %v, got %v", want, rc.params)
	}
}
F
function

TestNotFoundComponent

Parameters

router/router_test.go:129-143
func TestNotFoundComponent(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{Path: "/home", Component: func() core.Component { return &recordComponent{name: "home"} }})
	NavigateTo("/home")
	_ = mustRecord(t, CurrentComponent())

	NotFoundComponent = func() core.Component { return &recordComponent{name: "404"} }
	NavigateTo("/missing")

	nf := mustRecord(t, CurrentComponent())
	if nf.name != "404" {
		t.Fatalf("expected 404 current component, got %q", nf.name)
	}
}
F
function

TestTrailingSlashNormalization

Parameters

router/router_test.go:145-160
func TestTrailingSlashNormalization(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{Path: "/trail", Component: func() core.Component { return &recordComponent{name: "trail"} }})
	NavigateTo("/trail/")
	if got := mustRecord(t, CurrentComponent()).name; got != "trail" {
		t.Fatalf("expected trailing slash to match, got %q", got)
	}

	var called bool
	NotFoundCallback = func(string) { called = true }
	NavigateTo("/trail/extra")
	if !called {
		t.Fatalf("expected not found for extra segments")
	}
}
F
function

TestNestedRouteNavigation

Parameters

router/router_test.go:162-186
func TestNestedRouteNavigation(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{
		Path: "/teams/:team",
		Children: []Route{{
			Path:      "projects/:project",
			Component: func() core.Component { return &recordComponent{name: "project"} },
		}},
	})

	NavigateTo("/teams/acme/projects/rfw?tab=issues")
	rc := mustRecord(t, CurrentComponent())
	want := map[string]string{
		"team":    "acme",
		"project": "rfw",
		"tab":     "issues",
	}
	if !reflect.DeepEqual(rc.params, want) {
		t.Fatalf("expected params %v, got %v", want, rc.params)
	}
	if !CanNavigate("/teams/acme/projects/rfw") {
		t.Fatal("expected nested route to be navigable")
	}
}
F
function

TestNestedRouteChildParamShadowsParent

Parameters

router/router_test.go:188-204
func TestNestedRouteChildParamShadowsParent(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{
		Path: "/teams/:id",
		Children: []Route{{
			Path:      "users/:id",
			Component: func() core.Component { return &recordComponent{name: "user"} },
		}},
	})

	NavigateTo("/teams/acme/users/alice")
	rc := mustRecord(t, CurrentComponent())
	if rc.params["id"] != "alice" {
		t.Fatalf("expected child id=alice, got %v", rc.params)
	}
}
F
function

TestParentRouteDoesNotMatchUnknownChild

Parameters

router/router_test.go:206-225
func TestParentRouteDoesNotMatchUnknownChild(t *testing.T)

{
	resetRouter(t)

	RegisterRoute(Route{
		Path:      "/docs",
		Component: func() core.Component { return &recordComponent{name: "docs"} },
		Children: []Route{{
			Path:      "page",
			Component: func() core.Component { return &recordComponent{name: "page"} },
		}},
	})
	called := false
	NotFoundCallback = func(string) { called = true }

	NavigateTo("/docs/unknown")

	if !called {
		t.Fatal("expected unknown child path to be not found")
	}
}
F
function

TestAbsoluteNestedRouteNavigation

Parameters

router/router_test.go:227-247
func TestAbsoluteNestedRouteNavigation(t *testing.T)

{
	resetRouter(t)
	guardCalled := false
	RegisterRoute(Route{
		Path:   "/admin",
		Guards: []Guard{func(map[string]string) bool { guardCalled = true; return true }},
		Children: []Route{{
			Path:      "/login",
			Component: func() core.Component { return &recordComponent{name: "login"} },
		}},
	})

	NavigateTo("/login")

	if got := mustRecord(t, CurrentComponent()).name; got != "login" {
		t.Fatalf("expected absolute child route, got %q", got)
	}
	if !guardCalled {
		t.Fatal("expected parent guard to run for absolute child")
	}
}
S
struct

dataComponent

router/router_data_test.go:13-16
type dataComponent struct

Methods

SetRouteData
Method

Parameters

data any
func (*dataComponent) SetRouteData(data any)
{
	component.data = data
}

Fields

Name Type Description
data any
F
function

TestNamedRouteURLAndMetadata

Parameters

router/router_data_test.go:22-57
func TestNamedRouteURLAndMetadata(t *testing.T)

{
	resetRouter(t)
	RegisterRoute(Route{
		Path: "/teams/:team",
		Children: []Route{{
			Path:      "users/:user",
			Name:      "team-user",
			Component: func() core.Component { return &recordComponent{name: "user"} },
			Meta:      map[string]any{"title": "User"},
		}},
	})

	path, err := URL("team-user", map[string]string{
		"team": "core",
		"user": "Ada Lovelace",
	}, url.Values{"tab": {"activity"}})
	if err != nil {
		t.Fatalf("build URL: %v", err)
	}
	if path != "/teams/core/users/Ada%20Lovelace?tab=activity" {
		t.Fatalf("unexpected URL: %s", path)
	}
	if err := NavigateContext(context.Background(), "/teams/core/users/Ada%20Lovelace"); err != nil {
		t.Fatalf("navigate generated URL: %v", err)
	}
	component := CurrentComponent().(*recordComponent)
	if component.params["user"] != "Ada Lovelace" {
		t.Fatalf("route parameter was not decoded: %#v", component.params)
	}

	definitions := RegisteredRoutes()
	child := definitions[0].Children[0]
	if child.Name != "team-user" || child.Meta["title"] != "User" {
		t.Fatalf("named route metadata missing: %#v", child)
	}
}
F
function

TestRouteLoaderCommitsDataAndMeta

Parameters

router/router_data_test.go:59-91
func TestRouteLoaderCommitsDataAndMeta(t *testing.T)

{
	resetRouter(t)
	var loadedContext LoadContext
	RegisterRoute(Route{
		Path: "/reports/:id",
		Name: "report",
		Component: func() core.Component {
			return &dataComponent{recordComponent: recordComponent{name: "report"}}
		},
		Loader: func(_ context.Context, loadContext LoadContext) (any, error) {
			loadedContext = loadContext
			return map[string]any{"total": 4}, nil
		},
		Meta: map[string]any{"section": "reports"},
	})

	if err := NavigateContext(context.Background(), "/reports/7?period=week"); err != nil {
		t.Fatalf("navigate: %v", err)
	}
	component := CurrentComponent().(*dataComponent)
	if !reflect.DeepEqual(component.data, map[string]any{"total": 4}) {
		t.Fatalf("loader data missing: %#v", component.data)
	}
	if loadedContext.Params["id"] != "7" || loadedContext.Query.Get("period") != "week" {
		t.Fatalf("loader context incorrect: %#v", loadedContext)
	}
	if Status().Get() != NavigationReady || Error().Get() != nil {
		t.Fatalf("unexpected navigation state: status=%s error=%v", Status().Get(), Error().Get())
	}
	if Meta().Get()["section"] != "reports" {
		t.Fatalf("route metadata missing: %#v", Meta().Get())
	}
}
F
function

TestNewNavigationCancelsPreviousLoader

Parameters

router/router_data_test.go:93-124
func TestNewNavigationCancelsPreviousLoader(t *testing.T)

{
	resetRouter(t)
	started := make(chan struct{})
	RegisterRoute(Route{
		Path:      "/slow",
		Component: func() core.Component { return &recordComponent{name: "slow"} },
		Loader: func(ctx context.Context, _ LoadContext) (any, error) {
			close(started)
			<-ctx.Done()
			return nil, ctx.Err()
		},
	})
	RegisterRoute(Route{
		Path:      "/fast",
		Component: func() core.Component { return &recordComponent{name: "fast"} },
	})

	result := make(chan error, 1)
	go func() {
		result <- NavigateContext(context.Background(), "/slow")
	}()
	<-started
	if err := NavigateContext(context.Background(), "/fast"); err != nil {
		t.Fatalf("fast navigation: %v", err)
	}
	if err := <-result; !errors.Is(err, context.Canceled) {
		t.Fatalf("slow loader was not cancelled: %v", err)
	}
	if CurrentComponent().GetName() != "fast" || Status().Get() != NavigationReady {
		t.Fatalf("stale loader replaced current route: component=%v status=%s", CurrentComponent(), Status().Get())
	}
}
F
function

TestRouteRedirectInterpolatesParameters

Parameters

router/router_data_test.go:126-150
func TestRouteRedirectInterpolatesParameters(t *testing.T)

{
	resetRouter(t)
	RegisterRoute(Route{Path: "/legacy/:id", Redirect: "/users/:id"})
	RegisterRoute(Route{
		Path:      "/users/:id",
		Component: func() core.Component { return &dataComponent{recordComponent: recordComponent{name: "user"}} },
		Loader: func(ctx context.Context, load LoadContext) (any, error) {
			if err := ctx.Err(); err != nil {
				return nil, err
			}
			return load.Params["id"], nil
		},
	})

	if err := NavigateContext(context.Background(), "/legacy/42"); err != nil {
		t.Fatalf("redirect: %v", err)
	}
	component := CurrentComponent().(*dataComponent)
	if component.params["id"] != "42" || ActivePath().Get() != "/users/42" {
		t.Fatalf("redirect destination incorrect: component=%#v path=%s", component, ActivePath().Get())
	}
	if component.data != "42" {
		t.Fatalf("redirected loader did not complete: %#v", component.data)
	}
}
F
function

TestRouteRedirectLoopFails

Parameters

router/router_data_test.go:152-160
func TestRouteRedirectLoopFails(t *testing.T)

{
	resetRouter(t)
	RegisterRoute(Route{Path: "/loop-a", Redirect: "/loop-b"})
	RegisterRoute(Route{Path: "/loop-b", Redirect: "/loop-a"})

	if err := NavigateContext(context.Background(), "/loop-a"); !errors.Is(err, ErrRedirectLoop) {
		t.Fatalf("expected redirect loop error, got %v", err)
	}
}
F
function

TestCancelledNavigationDoesNotCommit

Parameters

router/router_data_test.go:162-176
func TestCancelledNavigationDoesNotCommit(t *testing.T)

{
	resetRouter(t)
	RegisterRoute(Route{
		Path:      "/cancelled",
		Component: func() core.Component { return &recordComponent{name: "cancelled"} },
	})
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	if err := NavigateContext(ctx, "/cancelled"); !errors.Is(err, context.Canceled) {
		t.Fatalf("expected cancelled context, got %v", err)
	}
	if CurrentComponent() != nil {
		t.Fatalf("cancelled navigation committed %#v", CurrentComponent())
	}
}
F
function

CurrentComponent

CurrentComponent returns the current routed component.

Returns

router/router_helpers.go:9-9
func CurrentComponent() core.Component

{ return currentComponent }
S
struct
Implements: routeParamHandler

reloadComponent

router/router_reload_test.go:11-11
type reloadComponent struct

Methods

Render
Method

Returns

string
func (*reloadComponent) Render() string
{ return "" }
Mount
Method
func (*reloadComponent) Mount()
{}
Unmount
Method
func (*reloadComponent) Unmount()
{}
OnMount
Method
func (*reloadComponent) OnMount()
{}
OnUnmount
Method
func (*reloadComponent) OnUnmount()
{}
GetName
Method

Returns

string
func (*reloadComponent) GetName() string
{ return "reload" }
GetID
Method

Returns

string
func (*reloadComponent) GetID() string
{ return "" }
SetSlots
Method

Parameters

map[string]any
func (*reloadComponent) SetSlots(map[string]any)
{}
IsMounted
Method

Returns

bool
func (*reloadComponent) IsMounted() bool
{ return false }
OnParams
Method

Parameters

map[string]string
func (*reloadComponent) OnParams(map[string]string)
{}
F
function

TestNavigateReloadsRouteEachTime

Parameters

router/router_reload_test.go:24-36
func TestNavigateReloadsRouteEachTime(t *testing.T)

{
	Reset()
	count := 0
	RegisterRoute(Route{Path: "/reload", Component: func() core.Component {
		count++
		return &reloadComponent{}
	}})
	Navigate("/reload")
	Navigate("/reload")
	if count != 2 {
		t.Fatalf("expected loader called twice, got %d", count)
	}
}