-
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.0import { 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