-
Notifications
You must be signed in to change notification settings - Fork 8
Juris Router
A powerful, reactive URL router designed for the Juris framework. This router provides comprehensive URL management with reactive state integration, multiple routing modes, and advanced features like route guards and parameter parsing.
- Multiple Routing Modes: Hash, History, and Memory routing
- Reactive State Integration: Automatic synchronization with Juris state management
- Route Guards: Global and route-specific navigation guards
- Advanced URL Parsing: Query parameters, route parameters, and path segments
- Performance Optimized: Debouncing, caching, and duplicate navigation prevention
- Scroll Position Management: Preserve and restore scroll positions
- Flexible Configuration: Extensive configuration options with sensible defaults
The router is designed as a headless component for the Juris framework. Ensure you have Juris and HeadlessManager available.
// Register the router as a headless component
juris.registerHeadlessComponent('router', Router, {
config: {
// Router configuration
}
});
// Initialize the router
const routerInstance = juris.initializeHeadlessComponent('router');const routerConfig = {
mode: 'hash',
defaultRoute: '/',
routes: {
'/': { component: 'HomePage' },
'/about': { component: 'AboutPage' },
'/users/:id': { component: 'UserProfile' }
}
};
juris.registerHeadlessComponent('router', Router, { config: routerConfig });
const router = juris.initializeHeadlessComponent('router');// Navigate to a route
router.api.navigate('/about');
// Navigate with parameters
router.api.navigate('/users/123');
// Replace current route
router.api.replace('/login');
// Browser navigation
router.api.back();
router.api.forward();
router.api.go(-2); // Go back 2 steps// Get current path
const currentPath = router.api.getCurrentPath();
// Get route parameters
const params = router.api.getParams(); // { id: '123' }
// Get query parameters
const query = router.api.getQuery(); // { tab: 'profile', sort: 'asc' }
// Get path segments
const segments = router.api.getSegments();
// { full: '/users/123/profile', parts: ['users', '123', 'profile'], base: 'users', sub: '123' }const config = {
// State management
statePath: 'url', // Base path in state where URL data is stored
stateStructure: {
path: 'path', // Key for current path
segments: 'segments', // Key for parsed segments
params: 'params', // Key for URL parameters
query: 'query', // Key for query string
hash: 'hash' // Key for hash fragment
},
// URL handling
mode: 'hash', // 'hash' | 'history' | 'memory'
basePath: '', // Base path for history mode
caseSensitive: false, // Case sensitive route matching
trailingSlash: 'ignore', // 'strict' | 'ignore' | 'redirect'
// Route configuration
routes: {}, // Route definitions
defaultRoute: '/', // Default route when none matches
notFoundRoute: '/404', // Route for 404 handling
// Performance options
debounceMs: 0, // Debounce URL changes
preventDuplicates: true, // Prevent duplicate navigation
preserveScrollPosition: false, // Restore scroll position
// Debug options
debug: false, // Enable debug logging
logPrefix: '🧭' // Prefix for log messages
};{
mode: 'hash'
// URLs: example.com/#/about, example.com/#/users/123
}{
mode: 'history',
basePath: '/app' // Optional base path
// URLs: example.com/app/about, example.com/app/users/123
}{
mode: 'memory'
// For testing or server-side rendering
}const routes = {
'/': { component: 'HomePage' },
'/about': { component: 'AboutPage' },
'/contact': { component: 'ContactPage' }
};const routes = {
'/users/:id': {
component: 'UserProfile',
guards: [requireAuth]
},
'/posts/:category/:slug': {
component: 'BlogPost',
metadata: { requiresAuth: true }
}
};// Add routes at runtime
router.api.addRoute('/admin/:section', {
component: 'AdminPanel',
guards: [requireAdmin]
});
// Remove routes
router.api.removeRoute('/admin/:section');
// Check if route exists
const exists = router.api.hasRoute('/admin');const config = {
globalGuards: {
beforeEnter: [
async (newUrl, oldUrl, routeMatch) => {
// Check authentication
const user = getState('user.current');
if (!user && newUrl.startsWith('/admin')) {
navigate('/login');
return false; // Block navigation
}
return true; // Allow navigation
}
],
afterEnter: [
(newUrl, oldUrl, routeMatch) => {
// Track page views
analytics.track('page_view', { path: newUrl });
}
]
}
};const requireAuth = async (newUrl, oldUrl, routeMatch) => {
const isAuthenticated = await checkAuth();
if (!isAuthenticated) {
router.api.navigate('/login');
return false;
}
return true;
};
const routes = {
'/dashboard': {
component: 'Dashboard',
guards: [requireAuth]
}
};The router automatically syncs URL changes with the Juris state system:
// React to URL changes in components
const MyComponent = (props, { getState }) => {
return {
div: {
text: () => {
const path = getState('url.path', '/');
return `Current page: ${path}`;
}
}
};
};
// Subscribe to route changes
juris.subscribe('url.path', (newPath, oldPath) => {
console.log(`Navigated from ${oldPath} to ${newPath}`);
});// Build URLs with parameters
const userUrl = router.api.buildUrl('/users/:id', { id: 123 }, { tab: 'profile' });
// Result: '/users/123?tab=profile'
// Parse URLs
const parsed = router.api.parseUrl('/users/123?tab=profile&sort=asc#section');
// Result: {
// path: '/users/123',
// params: { id: '123' },
// query: { tab: 'profile', sort: 'asc' },
// hash: 'section',
// segments: { full: '/users/123', parts: ['users', '123'], base: 'users', sub: '123' }
// }// Check if route is active
const isActive = router.api.isActive('/users'); // true for '/users/123'
const isExactActive = router.api.isActive('/users', true); // false for '/users/123'
// Use in components
const NavLink = (props, { getState }) => {
const isActive = () => router.api.isActive(props.to, props.exact);
return {
a: {
href: props.to,
class: () => isActive() ? 'nav-link active' : 'nav-link',
onclick: (e) => {
e.preventDefault();
router.api.navigate(props.to);
},
text: props.children
}
};
};const config = {
events: {
beforeChange: (newUrl, oldUrl) => {
console.log(`About to navigate from ${oldUrl} to ${newUrl}`);
// Return false to prevent navigation
},
afterChange: (newUrl, oldUrl) => {
console.log(`Navigated to ${newUrl}`);
// Update page title, analytics, etc.
},
onError: (context, error) => {
console.error(`Router error in ${context}:`, error);
},
onGuardFail: (newUrl, oldUrl) => {
console.log(`Navigation to ${newUrl} was blocked`);
}
}
};const config = {
segmentParsing: {
enabled: true,
maxDepth: 10,
customKeys: ['base', 'sub', 'section', 'item'],
includeEmpty: false
}
};
// For URL: /products/electronics/phones/iphone
// Results in segments: {
// full: '/products/electronics/phones/iphone',
// parts: ['products', 'electronics', 'phones', 'iphone'],
// base: 'products',
// sub: 'electronics',
// section: 'phones',
// item: 'iphone'
// }const config = {
preserveScrollPosition: true // Automatically save/restore scroll positions
};const config = {
syncOnStateChange: true // Update URL when state changes
};
// Now changing state will update the URL
juris.setState('url.path', '/new-page'); // Updates browser URLconst authGuard = async (newUrl, oldUrl, routeMatch) => {
const token = localStorage.getItem('authToken');
if (!token && routeMatch?.route?.requiresAuth) {
router.api.navigate('/login');
return false;
}
return true;
};const config = {
routes: {
'/404': { component: 'NotFoundPage' }
},
notFoundRoute: '/404'
};router.api.executeBatch(() => {
juris.setState('url.path', '/users/123');
juris.setState('url.query', { tab: 'profile' });
juris.setState('user.current', userData);
});const MyComponent = (props, context) => {
let unsubscribe;
return {
hooks: {
onMount: () => {
unsubscribe = juris.subscribe('url.path', (path) => {
console.log('Route changed:', path);
});
},
onUnmount: () => {
if (unsubscribe) unsubscribe();
}
},
render: () => ({ div: { text: 'Component content' } })
};
};-
navigate(path, options)- Navigate to a route -
replace(path, options)- Replace current route -
back()- Go back in history -
forward()- Go forward in history -
go(delta)- Go to specific history position
-
getCurrentPath()- Get current path -
getSegments()- Get parsed segments -
getParams()- Get route parameters -
getQuery()- Get query parameters
-
addRoute(path, config)- Add a route -
removeRoute(path)- Remove a route -
hasRoute(path)- Check if route exists -
matchRoute(path)- Match route pattern
-
buildUrl(path, params, query)- Build URL string -
parseUrl(url)- Parse URL components -
isActive(path, exact)- Check if route is active
Routes not matching: Check that your route patterns use :param syntax for parameters and that paths are properly normalized.
State not updating: Ensure the router is properly initialized and that you're subscribing to the correct state paths.
Guards not executing: Verify that guards return boolean values or promises that resolve to booleans.
Memory leaks: Make sure to clean up subscriptions and avoid creating new guard functions on every render.
Enable debug mode to see detailed logging:
const config = {
debug: true,
logPrefix: '🧭 ROUTER'
};This will log route changes, guard executions, and other internal operations to help with debugging.
MIT License - See the Juris framework documentation for full license details.