Skip to content

Router

The Router class provides hash-based client-side routing for Vio applications. It matches URL paths against route definitions, supports dynamic parameters, query strings, and route guards.

Import

ts
import { Router } from '@atrotos/vio'

TIP

You typically do not create a Router directly. Instead, pass a routes array to createApp and use app.navigate(). The Router class is exported for advanced use cases and testing.

Signature

ts
class Router {
  constructor(routes: Route[], bus: EventBus)
  setStoreGetter(getter: () => Record<string, unknown>): void
  resolve(path: string): RouteMatch | null
  navigate(path: string): RouteMatch | null
  getCurrentRoute(): RouteMatch | null
}

Constructor

ts
new Router(routes: Route[], bus: EventBus)
ParameterTypeRequiredDescription
routesRoute[]YesArray of route definitions
busEventBusYesEvent bus instance for emitting route events

Route Interface

ts
interface Route {
  path: string
  component: ComponentDef
  guard?: (store: Record<string, unknown>) => boolean
}
PropertyTypeRequiredDescription
pathstringYesURL path pattern. Supports static segments, dynamic params (:id), and wildcard (*).
componentComponentDefYesComponent to render when this route matches
guard(store: Record<string, unknown>) => booleanNoGuard function. If it returns false, the route is skipped. Receives the current store state.

Path Matching

  • Static paths: /about, /users -- exact match
  • Dynamic parameters: /users/:id -- captures :id as a param
  • Wildcard: * -- matches any path (useful as a catch-all / 404 route)

Routes are matched in order. The first matching route wins.

RouteMatch Interface

ts
interface RouteMatch {
  component: ComponentDef
  params: Record<string, string>
  path: string
  query: Record<string, string>
}
PropertyTypeDescription
componentComponentDefThe matched route's component definition
paramsRecord<string, string>Dynamic path parameters (e.g., { id: '42' })
pathstringThe matched pathname (without query string)
queryRecord<string, string>Parsed query string parameters

Methods

setStoreGetter(getter)

Sets a function that the router calls to get the current store state when evaluating route guards.

ts
setStoreGetter(getter: () => Record<string, unknown>): void
ParameterTypeDescription
getter() => Record<string, unknown>Function returning the current store state

TIP

This is called automatically by createApp when both a store and routes are configured.

resolve(path)

Resolves a path against the route definitions without triggering navigation or emitting events. Returns the matching RouteMatch or null.

ts
resolve(path: string): RouteMatch | null
ParameterTypeDescription
pathstringPath to resolve, optionally with a query string (e.g., /users/42?tab=posts)
ts
const match = router.resolve('/users/42?tab=posts')
if (match) {
  console.log(match.params)  // { id: '42' }
  console.log(match.query)   // { tab: 'posts' }
}

Navigates to a path. Updates the current route and emits route lifecycle events. Returns the matching RouteMatch or null if no route matched.

ts
navigate(path: string): RouteMatch | null
ParameterTypeDescription
pathstringPath to navigate to
ts
const match = router.navigate('/about')

getCurrentRoute()

Returns the current RouteMatch or null if no navigation has occurred.

ts
getCurrentRoute(): RouteMatch | null
ts
const current = router.getCurrentRoute()
if (current) {
  console.log('Currently at:', current.path)
}

Events Emitted

The router emits three events during navigation (in order):

EventPayloadDescription
route:before{ from: string | null, to: string }Before the route change is applied
route:change{ from: string | null, to: string, params: Record<string, string> }After the current route is updated
route:after{ path: string, params: Record<string, string> }After navigation is complete

Full Example

ts
import { createApp, defineComponent } from '@atrotos/vio'

const Home = defineComponent({
  name: 'Home',
  render: () => ({ tag: 'h1', children: ['Home'] })
})

const UserProfile = defineComponent({
  name: 'UserProfile',
  render: (state) => ({
    tag: 'div',
    children: [
      { tag: 'h1', children: [`User ${state.userId}`] }
    ]
  })
})

const NotFound = defineComponent({
  name: 'NotFound',
  render: () => ({ tag: 'h1', children: ['404 - Not Found'] })
})

const Dashboard = defineComponent({
  name: 'Dashboard',
  render: () => ({ tag: 'h1', children: ['Dashboard'] })
})

const app = createApp({
  root: '#app',
  routes: [
    { path: '/', component: Home },
    { path: '/users/:id', component: UserProfile },
    {
      path: '/dashboard',
      component: Dashboard,
      guard: (store) => store.isAuthenticated === true
    },
    { path: '*', component: NotFound }
  ],
  store: {
    state: { isAuthenticated: false },
    actions: {
      login: (state) => ({ ...state, isAuthenticated: true }),
      logout: (state) => ({ ...state, isAuthenticated: false })
    }
  }
})

app.on('route:change', (e) => {
  console.log(`Navigated from ${e.payload.from} to ${e.payload.to}`)
})

app.mount()

Released under the MIT License.