-
Notifications
You must be signed in to change notification settings - Fork 8
Juris Router
Advanced Routing for the Juris Reactive Framework
A sophisticated headless router component that delivers enterprise-grade URL management with deep reactive state integration. Designed for the Juris framework's "Object-First Architecture" with zero-build deployment and AI collaboration readiness.
- Headless Architecture: Pure state-driven routing without UI dependencies
- Reactive State Integration: Automatic synchronization with Juris's reactive state system
- Multiple Routing Modes: Hash, History API, and Memory routing
- Advanced URL Parsing: Query parameters, route parameters, and intelligent path segments
- Route Guards System: Global and route-specific navigation protection
- Performance Optimized: Built-in debouncing, caching, and duplicate prevention
- Scroll Position Management: Automatic preservation and restoration
- Temporal Independence: Component-agnostic routing that works with any UI pattern
- Progressive Enhancement Ready: Enhance existing applications without breaking changes
- AI Collaboration Ready: Designed for seamless AI-assisted development
<!-- Core Juris Framework --> <script src="https://unpkg.com/juris@0.9.0/juris.js"></script> <!-- Headless Component Support --> <script src="https://unpkg.com/juris@0.9.0/juris-headless.js"></script> <!-- Router Component --> <script src="https://unpkg.com/juris@0.9.0/headless/juris-router.js"></script>
npm install juris@0.9.0
import { Juris } from 'juris/juris'; import { HeadlessManager } from 'juris/juris-headless'; import { Router } from 'juris/headless/juris-router';
const juris = new Juris({ logLevel: 'warn', features: { headless: HeadlessManager }, headlessComponents: { router: { fn: Router, options: { autoInit: true, config: { mode: 'hash', routes: { '/': { name: 'Home' }, '/products': { name: 'Products' }, '/users/:id': { name: 'User Profile' }, '/404': { name: 'Not Found' } }, defaultRoute: '/', notFoundRoute: '/404' } } } }, states: { currentUser: null, products: [] }, layout: { AppLayout: {} } }); // Access router API const router = juris.getHeadlessAPI('router');
// Reactive navigation component using Juris's Object-First Architecture const Navigation = (props, { getState }) => { return { nav: { class: 'main-nav', children: [ { a: { href: '#/', class: () => router.isActive('/') ? 'nav-link active' : 'nav-link', onclick: (e) => { e.preventDefault(); router.navigate('/'); }, text: 'Home' } }, { a: { href: '#/products', class: () => router.isActive('/products') ? 'nav-link active' : 'nav-link', onclick: (e) => { e.preventDefault(); router.navigate('/products'); }, text: 'Products' } } ] } }; };
Perfect for static hosting and maximum compatibility:
{ mode: 'hash' // URLs: example.com/#/, example.com/#/products }
For modern applications with server-side routing support:
{ mode: 'history', basePath: '/app' // URLs: example.com/app/, example.com/app/products }
Ideal for testing and server-side rendering:
{ mode: 'memory' // In-memory routing without URL changes }
The router leverages Juris's reactive state system for automatic UI updates:
// Component that reacts to route changes const PageContent = (props, { getState }) => { return { main: { class: 'page-content', children: () => { const currentPath = getState('url.path', '/'); const params = getState('url.params', {}); switch (currentPath) { case '/': return [{ HomePage: {} }]; case '/products': return [{ ProductList: {} }]; default: if (currentPath.startsWith('/users/')) { return [{ UserProfile: { userId: params.id } }]; } return [{ NotFound: {} }]; } } } }; }; // Automatic subscription to route changes juris.subscribe('url.path', (newPath, oldPath) => { console.log(`Route changed from ${oldPath} to ${newPath}`); });
const requireAuth = async (newUrl, oldUrl, routeMatch) => { const user = juris.getState('currentUser'); if (!user && routeMatch.route.requiresAuth) { router.navigate('/login'); return false; // Block navigation } return true; // Allow navigation }; const config = { routes: { '/dashboard': { name: 'Dashboard', requiresAuth: true, guards: [requireAuth] } }, globalGuards: { beforeEnter: [requireAuth], afterEnter: [ (newUrl) => { // Analytics tracking analytics.track('page_view', { path: newUrl }); } ], beforeLeave: [ (newUrl, oldUrl, routeMatch) => { // Confirm leaving unsaved changes const hasUnsavedChanges = juris.getState('form.hasChanges', false); if (hasUnsavedChanges) { return confirm('You have unsaved changes. Are you sure you want to leave?'); } return true; } ] } };
The router provides comprehensive event callbacks for monitoring and controlling navigation:
const config = { events: { beforeChange: (newUrl, oldUrl) => { console.log(`About to navigate from ${oldUrl} to ${newUrl}`); // Conditional navigation prevention const isFormDirty = juris.getState('form.isDirty', false); if (isFormDirty && !confirm('Discard unsaved changes?')) { return false; // Prevent navigation } // Set loading state juris.setState('ui.isNavigating', true); return true; // Allow navigation }, afterChange: (newUrl, oldUrl) => { console.log(`Successfully navigated to ${newUrl}`); // Clear loading state juris.setState('ui.isNavigating', false); // Update page metadata const routeMatch = router.matchRoute(newUrl); if (routeMatch?.route?.name) { document.title = `App - ${routeMatch.route.name}`; } // Analytics tracking if (typeof gtag !== 'undefined') { gtag('config', 'GA_MEASUREMENT_ID', { page_path: newUrl }); } // Clear error states juris.setState('ui.error', null); }, onError: (context, error) => { console.error(`Router error in ${context}:`, error); // Set error state for user feedback juris.setState('ui.error', { message: `Navigation error: ${error.message}`, context: context, timestamp: Date.now() }); // Optional: Navigate to error page if (context === 'route-matching' && router.hasRoute('/error')) { router.navigate('/error'); } }, onGuardFail: (newUrl, oldUrl) => { console.log(`Navigation to ${newUrl} was blocked by guards`); // User feedback for blocked navigation juris.setState('ui.notification', { type: 'warning', message: 'Access denied. Please check your permissions.', duration: 3000 }); // Optional: Redirect to appropriate page const user = juris.getState('currentUser'); if (!user) { router.navigate('/login'); } else { router.navigate('/unauthorized'); } } } };
Understanding when to use guards vs event callbacks:
const config = { // Global Guards - Control navigation flow globalGuards: { beforeEnter: [ // Use for authentication/authorization async (newUrl, oldUrl, routeMatch) => { const user = await getCurrentUser(); const route = routeMatch?.route; if (route?.requiresAuth && !user) { router.navigate('/login'); return false; // Block navigation } if (route?.requiredRoles) { const hasRole = route.requiredRoles.some(role => user?.roles?.includes(role) ); if (!hasRole) { return false; // Block navigation } } return true; // Allow navigation } ], afterEnter: [ // Use for side effects after successful navigation (newUrl, oldUrl, routeMatch) => { // Log user activity logUserActivity({ action: 'navigate', from: oldUrl, to: newUrl, timestamp: new Date().toISOString() }); // Update user preferences const user = juris.getState('currentUser'); if (user) { updateUserPreference('lastVisitedPage', newUrl); } } ], beforeLeave: [ // Use for cleanup or confirmation before leaving (newUrl, oldUrl, routeMatch) => { // Auto-save drafts const draftContent = juris.getState('editor.content'); if (draftContent && oldUrl.includes('/editor')) { saveDraft(draftContent); } // Confirm leaving active processes const activeDownloads = juris.getState('downloads.active', []); if (activeDownloads.length > 0) { return confirm(`${activeDownloads.length} downloads are active. Leave anyway?`); } return true; } ] }, // Event Callbacks - Monitor and react to navigation events: { beforeChange: (newUrl, oldUrl) => { // Use for UI state management juris.executeBatch(() => { juris.setState('ui.isNavigating', true); juris.setState('ui.previousUrl', oldUrl); juris.setState('ui.navigationStartTime', Date.now()); }); }, afterChange: (newUrl, oldUrl) => { // Use for post-navigation updates const navigationTime = Date.now() - juris.getState('ui.navigationStartTime', 0); juris.executeBatch(() => { juris.setState('ui.isNavigating', false); juris.setState('ui.navigationTime', navigationTime); juris.setState('analytics.pageViews', prev => (prev || 0) + 1); }); // Performance monitoring if (navigationTime > 1000) { console.warn(`Slow navigation detected: ${navigationTime}ms to ${newUrl}`); } } } };
const config = { queryStateSync: { enabled: true, stateBasePath: '__state', debounceMs: 150, parseTypes: true, encodeArrays: true, excludeEmpty: true, includeInHistory: true } }; // Query parameters automatically sync with state // URL: /products?category=electronics&sort=price // State: { __state: { category: 'electronics', sort: 'price' }}
const config = { segmentParsing: { enabled: true, maxDepth: 10, customKeys: ['base', 'sub', 'section', 'item'], includeEmpty: false } }; // For URL: /products/electronics/phones/iphone // Results in reactive state: // { // url: { // segments: { // full: '/products/electronics/phones/iphone', // parts: ['products', 'electronics', 'phones', 'iphone'], // base: 'products', // sub: 'electronics', // section: 'phones', // item: 'iphone' // } // } // }
Enhance existing HTML without breaking changes:
// Enhance existing navigation juris.enhance('nav a', (element, { getState, setState }) => { return { onclick: (e) => { e.preventDefault(); const href = element.getAttribute('href'); router.navigate(href.replace('#', '')); }, class: () => { const href = element.getAttribute('href').replace('#', ''); return router.isActive(href) ? 'active' : ''; } }; });
// Navigate to routes router.navigate('/products'); router.navigate('/users/123'); router.replace('/login'); // Replace current history entry // Browser navigation router.back(); router.forward(); router.go(-2); // Go back 2 steps
// Get current route information const path = router.getCurrentPath(); const params = router.getParams(); // { id: '123' } const query = router.getQuery(); // { tab: 'profile', sort: 'asc' } const segments = router.getSegments(); // Parsed path segments
// Dynamic route management router.addRoute('/admin/:section', { name: 'Admin Panel', guards: [requireAdmin] }); router.removeRoute('/admin/:section'); const exists = router.hasRoute('/admin');
// URL building and parsing const url = router.buildUrl('/users/:id', { id: 123 }, { tab: 'profile' }); // Result: '/users/123?tab=profile' const parsed = router.parseUrl('/users/123?tab=profile'); // Result: { path: '/users/123', params: { id: '123' }, query: { tab: 'profile' } } // Active route detection const isActive = router.isActive('/products'); const isExactActive = router.isActive('/products', true);
Use Juris's ARM system for global event handling with router integration:
// Enhanced event handling with router context const windowEvents = juris.arm(window, ({ getState, setState, router }) => ({ onpopstate: (e) => { // Handle browser back/forward with full context const newPath = router.getCurrentPath(); setState('navigation.browserNavigation', true); }, onhashchange: (e) => { // Custom hash handling const hash = window.location.hash; router.navigate(hash.substring(1)); } }));
const routerConfig = { // Core routing settings mode: 'hash', // 'hash' | 'history' | 'memory' basePath: '', // Base path for history mode caseSensitive: false, // Case sensitive matching trailingSlash: 'ignore', // 'strict' | 'ignore' | 'redirect' // Route definitions routes: {}, defaultRoute: '/', notFoundRoute: '/404', // State management integration statePath: 'url', stateStructure: { path: 'path', segments: 'segments', params: 'params', query: 'query', hash: 'hash' }, // Performance optimization debounceMs: 0, // Debounce URL changes preventDuplicates: true, // Prevent duplicate navigation preserveScrollPosition: false, // Restore scroll positions // Query state synchronization queryStateSync: { enabled: false, stateBasePath: '__state', debounceMs: 150, parseTypes: true, encodeArrays: true, excludeEmpty: true, includeInHistory: true }, // Segment parsing segmentParsing: { enabled: true, maxDepth: 10, customKeys: ['base', 'sub', 'section', 'item'], includeEmpty: false }, // Event callbacks events: { beforeChange: null, afterChange: null, onError: null, onGuardFail: null }, // Route guards globalGuards: { beforeEnter: [], afterEnter: [], beforeLeave: [] }, // Debug options debug: false, logPrefix: '🧭' };
// Use objects to define reactive routing components const RouteAwareComponent = (props, { getState }) => ({ div: { class: () => `page page-${getState('url.segments.base', 'home')}`, children: () => { const path = getState('url.path'); return path === '/dashboard' ? [{ Dashboard: props }] : [{ PublicPage: props }]; } } });
// Components should work regardless of routing state const UserProfile = (props, { getState }) => { // Get user ID from props OR route params const userId = props.userId || getState('url.params.id'); return { div: { class: 'user-profile', text: () => { const user = getState(`users.${userId}`); return user ? `Welcome, ${user.name}` : 'Loading...'; } } }; };
const securityGuards = { requireAuth: async (newUrl, oldUrl, routeMatch) => { const isAuthenticated = await checkAuthStatus(); if (!isAuthenticated && routeMatch.route.requiresAuth) { router.navigate('/login'); return false; } return true; } };
Enable comprehensive logging:
const config = { debug: true, logPrefix: '🧭 ROUTER' };
Routes not matching: Verify route patterns use :param syntax and paths are normalized.
State not updating: Ensure proper Juris state subscription patterns.
Guards failing: Check that guards return boolean values or promises resolving to booleans.
Memory leaks: Clean up subscriptions in component lifecycle hooks.
The Juris Router is designed specifically for the Juris framework's architecture:
- Object-First: Define routing logic using pure JavaScript objects
- Temporal Independence: Works with any component lifecycle
- Reactive Integration: Automatic state synchronization
- Progressive Enhancement: Enhance existing HTML without breaking changes
- AI Collaboration Ready: Structured for AI-assisted development
- [Juris Framework Documentation](https://jurisjs.com)
- [Interactive Router Examples](https://codepen.io/jurisauthor)
- [GitHub Repository](https://github.com/jurisjs/juris)
- [Online Testing Platform](https://jurisjs.com/tests/juris_pure_test_interface.html)
MIT License - Part of the Juris JavaScript Unified Reactive Interface Solution