Route Management SPA+
Client-side routing with history/hash mode support, guards, events, and named routes. For centralized configuration, view transitions, and advanced features, see Advanced Routing.
Overview
WildflowerJS includes a powerful RouteManager for single-page applications:
- History & Hash modes - Choose based on your server setup
- Dynamic parameters -
/users/:idwith optional params/:section? - Route guards - Block or redirect navigation conditionally
- DOM events - Components can listen for
route:afterChange - Centralized config - Define routes in a single configuration object
- Named routes - Navigate by name instead of path
Basic Setup
wildflower.createRouter() supports two equivalent patterns. Pick whichever matches the shape of your app.
Declarative form (auto-initialized)
Pass all routes up front in the routes: array. The router initializes itself and matches the current URL immediately; no .init() call needed.
const router = wildflower.createRouter({
mode: 'history', // or 'hash' for #/path URLs
base: '/', // base path for the application
routes: [
{ path: '/', name: 'home', handler: () => console.log('Home page') },
{ path: '/about', name: 'about', handler: () => console.log('About page') },
{ path: '/users/:id', name: 'user', handler: ({ params }) => console.log('User:', params.id) }
]
});
Staged form (caller controls init timing)
Omit routes:, register routes one by one with .onRoute(), then call .init() yourself. Use this when route handlers reference values that aren't ready at construction time (a global, a store, the router itself), or when routes are added dynamically.
createRouter auto-initializes, it tries to match the current URL right away. If the route tree is still empty (because you planned to register routes after construction), every page load would log a "No route matched" warning. Omitting routes: tells the factory to skip auto-init and wait for your explicit .init() call after .onRoute() registration is complete.
const router = wildflower.createRouter({
mode: 'history',
base: '/'
// no routes: array, no auto-init
});
router
.onRoute('/', {
name: 'home',
handler: () => console.log('Home page')
})
.onRoute('/about', {
name: 'about',
handler: () => console.log('About page')
})
.onRoute('/users/:id', {
name: 'user',
handler: ({ params }) => console.log('User:', params.id)
});
// Initialize when registration is done
router.init();
Route Parameters
Extract dynamic values from URLs:
Required Parameters
router.onRoute('/users/:id', {
name: 'user',
handler: ({ params }) => {
console.log(params.id); // "123" for /users/123
}
});
router.onRoute('/posts/:category/:slug', {
name: 'post',
handler: ({ params }) => {
console.log(params.category, params.slug);
// "tech", "hello-world" for /posts/tech/hello-world
}
});
Optional Parameters with Defaults
router.onRoute('/docs/:section?', {
defaults: { section: 'introduction' },
handler: ({ params }) => {
console.log(params.section);
// "introduction" for /docs
// "api" for /docs/api
}
});
Query Parameters
router.onRoute('/search', {
name: 'search',
handler: ({ query }) => {
console.log(query.q, query.page);
// "hello", "2" for /search?q=hello&page=2
}
});
// Navigate with query params
router.navigate('/search', { query: { q: 'hello', page: 2 } });
Route Guards
Control navigation with guards that can allow, block, or redirect:
Global Guards
// Runs before every navigation
router.beforeEach(({ to, from }) => {
// Check authentication
if (to.meta.requiresAuth && !isLoggedIn()) {
return '/login'; // Redirect to login
}
return true; // Allow navigation
});
// Runs after every navigation
router.afterEach(({ to, from }) => {
// Analytics, logging, etc.
analytics.pageView(to.path);
});
Per-Route Guards
router.onRoute('/admin', {
meta: { requiresAuth: true, role: 'admin' },
beforeEnter: ({ to }) => {
if (!hasRole('admin')) {
return '/unauthorized';
}
return true;
},
handler: () => showAdminDashboard()
});
Guard Return Values
| Return Value | Effect |
|---|---|
true / undefined | Allow navigation |
false | Block navigation |
'/path' | Redirect to path |
{ name: 'route' } | Redirect to named route |
Route Events
WildflowerJS provides two ways for components to react to route changes:
Option 1: onRouteChange Lifecycle Hook (Recommended)
The cleanest approach - define an onRouteChange method in your component:
wildflower.component('my-component', {
state: {
currentPage: ''
},
// Called automatically when route changes
onRouteChange(to, from) {
console.log(`Navigated from ${from?.path} to ${to.path}`);
this.currentPage = to.path;
// Access route params and query
if (to.params.id) {
this.loadUser(to.params.id);
}
}
});
Option 2: addEventListener (For Advanced Use Cases)
For non-component code or when you need more control:
// Listen for navigation completion
document.addEventListener('route:afterChange', (e) => {
const { to, from } = e.detail;
console.log(`Navigated from ${from?.path} to ${to.path}`);
// Update UI, breadcrumbs, analytics, etc.
updateBreadcrumbs(to);
});
// Cancel navigation conditionally
document.addEventListener('route:beforeChange', (e) => {
if (hasUnsavedChanges()) {
e.preventDefault(); // Block navigation
showSavePrompt();
}
});
// Handle redirects
document.addEventListener('route:redirect', (e) => {
console.log(`Redirected from ${e.detail.from} to ${e.detail.to}`);
});
// Handle errors
document.addEventListener('route:error', (e) => {
console.error('Navigation error:', e.detail.error);
});
Available Events
| Event | When | Cancelable | Detail |
|---|---|---|---|
route:beforeChange |
Before navigation starts | Yes | { to, from } |
route:afterChange |
After navigation completes | No | { to, from } |
route:redirect |
When redirect occurs | No | { from, to } (paths) |
route:error |
When navigation fails | No | { error, to, from } |
route:afterChange and updates automatically!
Named Routes
Navigate using route names instead of hardcoded paths:
// Define named routes
router.onRoute('/users/:id/posts/:postId', {
name: 'user-post',
handler: ({ params }) => showUserPost(params.id, params.postId)
});
// Navigate by name with params
router.navigate({
name: 'user-post',
params: { id: 123, postId: 456 }
});
// Generate URL without navigating
const url = router.getRouteUrl('user-post', { id: 123, postId: 456 });
// Returns: /users/123/posts/456
// Navigate with query parameters
router.navigate({
name: 'user-post',
params: { id: 123, postId: 456 },
query: { tab: 'comments' }
});
// Navigates to: /users/123/posts/456?tab=comments
// Check if route is active
if (router.isActive('/users')) {
// Current path starts with /users
}
Best Practices
Choose the Right Mode
| Mode | URLs | Best For |
|---|---|---|
history |
/users/123 |
Production with server config for SPA fallback |
hash |
/#/users/123 |
Static hosting, no server config needed |
Do's
- Use named routes - Easier refactoring when paths change
- Centralize route config - Single source of truth for navigation
- Use meta for auth -
meta: { requiresAuth: true } - Listen for events - Keep components decoupled from router
- Handle 404s - Use wildcard route
*as catch-all
Don'ts
- Don't manipulate history directly - Use
router.navigate() - Don't block in afterEach - It's for side effects only
- Don't forget init() - Router won't work without initialization