Skip to content

Juris Router

jurisauthor edited this page Aug 26, 2025 · 2 revisions

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.

Features

  • 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

Installation

CDN (Instant Deployment)

<!-- 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 Installation

npm install juris@0.9.0
import { Juris } from 'juris/juris';
import { HeadlessManager } from 'juris/juris-headless';
import { Router } from 'juris/headless/juris-router';

Quick Start

Basic Router Setup

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');

Object-First Component Integration

// 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'
                    }
                }
            ]
        }
    };
};

Routing Modes

Hash Mode (Default)

Perfect for static hosting and maximum compatibility:

{
    mode: 'hash'
    // URLs: example.com/#/, example.com/#/products
}

History Mode

For modern applications with server-side routing support:

{
    mode: 'history',
    basePath: '/app'
    // URLs: example.com/app/, example.com/app/products
}

Memory Mode

Ideal for testing and server-side rendering:

{
    mode: 'memory'
    // In-memory routing without URL changes
}

Reactive State Integration

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}`);
});

Route Guards and Security

Authentication Guards

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;
            }
        ]
    }
};

Event Callbacks

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');
            }
        }
    }
};

Global Guards vs Event Callbacks

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}`);
            }
        }
    }
};

Advanced Features

Query State Synchronization

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' } }

Intelligent Segment Parsing

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'
//     }
//   }
// }

Progressive Enhancement

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' : '';
        }
    };
});

API Reference

Navigation Methods

// 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

State Access Methods

// 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

Route Management

// Dynamic route management
router.addRoute('/admin/:section', {
    name: 'Admin Panel',
    guards: [requireAdmin]
});

router.removeRoute('/admin/:section');
const exists = router.hasRoute('/admin');

Utility Methods

// 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);

ARM (Advanced Reactive Management)

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));
    }
}));

Configuration Options

Complete Configuration Example

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: '🧭'
};

Best Practices

1. Leverage Juris's Object-First Architecture

// 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 }];
        }
    }
});

2. Implement Temporal Independence

// 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...';
            }
        }
    };
};

3. Use Route Guards for Security

const securityGuards = {
    requireAuth: async (newUrl, oldUrl, routeMatch) => {
        const isAuthenticated = await checkAuthStatus();
        if (!isAuthenticated && routeMatch.route.requiresAuth) {
            router.navigate('/login');
            return false;
        }
        return true;
    }
};

Troubleshooting

Debug Mode

Enable comprehensive logging:

const config = {
    debug: true,
    logPrefix: '🧭 ROUTER'
};

Common Issues

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.

Framework Integration

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

Resources

License

MIT License - Part of the Juris JavaScript Unified Reactive Interface Solution

Clone this wiki locally