@symbo.ls/router
@domql/router
Client-side router plugin for DOMQL. Handles route matching, navigation, scroll management, and state updates within DOMQL elements.
Install
npm install @domql/router
Basic Usage
import { router } from '@domql/router'
// Navigate to a path
router('/about', element)
// With state and options
router('/dashboard', element, { userId: 1 }, { scrollToTop: true })
Define routes on your DOMQL element:
const App = {
routes: {
'/': HomePage,
'/about': AboutPage,
'/contact': ContactPage,
'/*': NotFoundPage
}
}
Dynamic Route Params
Match routes with :param segments. Enable with useParamsMatching: true.
const App = {
routes: {
'/': HomePage,
'/:id': UserPage,
'/:category/:slug': ArticlePage,
'/*': NotFoundPage
}
}
router('/users/42', element, {}, { useParamsMatching: true })
// state.params = { id: '42' }
router('/tech/my-article', element, {}, { useParamsMatching: true })
// state.params = { category: 'tech', slug: 'my-article' }
Exact segments score higher than params, so /about will match a literal /about route before /:id.
Query String Parsing
Query parameters are automatically parsed and stored in state.
router('/search?q=hello&tag=a&tag=b', element)
// state.query = { q: 'hello', tag: ['a', 'b'] }
Duplicate keys are collected into arrays.
Guards / Middleware
Run async guard functions before navigation. Return true to allow, false to block, or a string to redirect.
const authGuard = ({ element }) => {
if (!element.state.root.isLoggedIn) return '/login'
return true
}
const roleGuard = ({ params }) => {
if (params.section === 'admin') return false
return true
}
router('/dashboard', element, {}, {
guards: [authGuard, roleGuard]
})
Guard functions receive a context object:
{
pathname, // full pathname
route, // matched route key
params, // dynamic route params
query, // parsed query string
hash, // URL hash
element, // DOMQL element
state // navigation state
}
404 Handling
Provide an onNotFound callback for unmatched routes:
router('/unknown', element, {}, {
onNotFound: ({ pathname, route, element }) => {
console.warn(`No route found for ${pathname}`)
}
})
You can also define a /* wildcard route as a catch-all fallback.
Custom Router Element
Use customRouterElement in your config.js to route pages into a specific element within the component tree instead of the root. This is useful for persistent layouts where only the content area changes between routes.
// symbols/config.js
export default {
router: {
customRouterElement: 'Folder.Content'
}
}
The value is a dot-separated path resolved from the root element. For example, 'Folder.Content' means the router will find root.Folder.Content and render page content inside it.
This allows you to define a layout once in the / (main) page and have sub-pages render inside a specific container:
// pages/main.js — defines the persistent layout
export const main = {
extends: 'Layout',
Folder: {
Content: {
// default content for '/' route
}
}
}
// pages/team.js — only defines content (rendered inside Folder.Content)
export const team = {
state: 'team',
extends: 'Grid',
childExtends: 'TeamItem',
childrenAs: 'state',
children: (el, s) => s.data,
}
Options
| Option | Type | Default | Description |
|---|---|---|---|
level |
number |
0 |
Route nesting level (which path segment to match) |
pushState |
boolean |
true |
Push to browser history |
initialRender |
boolean |
false |
Whether this is the initial page render |
scrollToTop |
boolean |
true |
Scroll to top after navigation |
scrollToNode |
boolean |
false |
Scroll within the element node |
scrollNode |
Element |
document.documentElement |
Node to scroll |
scrollToOffset |
number |
0 |
Offset when scrolling to hash anchors |
scrollToOptions |
object |
{ behavior: 'smooth' } |
Options passed to scrollTo() |
useFragment |
boolean |
false |
Use fragment tag for content |
updateState |
boolean |
true |
Update element state on navigation |
contentElementKey |
string |
'content' |
Key for the content element slot |
removeOldElement |
boolean |
false |
Remove old content element before setting new |
useParamsMatching |
boolean |
false |
Enable dynamic :param route matching |
guards |
function[] |
undefined |
Array of guard/middleware functions |
onNotFound |
function |
undefined |
Callback when no route matches |
Exported Utilities
getActiveRoute(level, route)
Returns the active route segment at the given nesting level.
import { getActiveRoute } from '@domql/router'
getActiveRoute(0, '/users/42') // '/users'
getActiveRoute(1, '/users/42') // '/42'
parseQuery(search)
Parses a query string into an object.
import { parseQuery } from '@domql/router'
parseQuery('?page=1&sort=name') // { page: '1', sort: 'name' }
matchRoute(pathname, routes, level)
Matches a pathname against a routes object. Returns { key, content, params }.
import { matchRoute } from '@domql/router'
const routes = { '/': Home, '/:id': Detail, '/*': NotFound }
const result = matchRoute('/42', routes)
// { key: '/:id', content: Detail, params: { id: '42' } }
parseRoutePattern(pattern)
Parses a route pattern string into segments, param definitions, and wildcard flag. Results are cached.
runGuards(guards, context)
Runs an array of async guard functions sequentially. Returns true, false, or a redirect path string.
Events
The router triggers an onRouteChanged event on the element after navigation completes. Listen for it in your element definition:
const App = {
routes: { ... },
onRouteChanged: (element, options) => {
console.log('Route changed:', element.state.route)
}
}
License
MIT