Skip to content

Juris Router

jurisauthor edited this page Aug 26, 2025 · 2 revisions

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.

Features

  • 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

Installation

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

Basic Usage

Simple Hash Router Setup

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

Navigation

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

Accessing Route Data

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

Configuration Options

Core Configuration

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

Routing Modes

Hash Mode (Default)

{
    mode: 'hash'
    // URLs: example.com/#/about, example.com/#/users/123
}

History Mode

{
    mode: 'history',
    basePath: '/app'  // Optional base path
    // URLs: example.com/app/about, example.com/app/users/123
}

Memory Mode

{
    mode: 'memory'
    // For testing or server-side rendering
}

Route Definitions

Basic Routes

const routes = {
    '/': { component: 'HomePage' },
    '/about': { component: 'AboutPage' },
    '/contact': { component: 'ContactPage' }
};

Parameterized Routes

const routes = {
    '/users/:id': { 
        component: 'UserProfile',
        guards: [requireAuth]
    },
    '/posts/:category/:slug': { 
        component: 'BlogPost',
        metadata: { requiresAuth: true }
    }
};

Dynamic Route Management

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

Route Guards

Global Guards

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

Route-Specific Guards

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

State Integration

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

URL Building and Parsing

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

Active Route Detection

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

Event Callbacks

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

Advanced Features

Segment Parsing

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

Scroll Position Management

const config = {
    preserveScrollPosition: true // Automatically save/restore scroll positions
};

State Synchronization

const config = {
    syncOnStateChange: true // Update URL when state changes
};

// Now changing state will update the URL
juris.setState('url.path', '/new-page'); // Updates browser URL

Best Practices

1. Use Route Guards for Authentication

const authGuard = async (newUrl, oldUrl, routeMatch) => {
    const token = localStorage.getItem('authToken');
    if (!token && routeMatch?.route?.requiresAuth) {
        router.api.navigate('/login');
        return false;
    }
    return true;
};

2. Handle 404s Gracefully

const config = {
    routes: {
        '/404': { component: 'NotFoundPage' }
    },
    notFoundRoute: '/404'
};

3. Use Batch Updates for Multiple State Changes

router.api.executeBatch(() => {
    juris.setState('url.path', '/users/123');
    juris.setState('url.query', { tab: 'profile' });
    juris.setState('user.current', userData);
});

4. Clean Up Subscriptions

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

API Reference

Navigation Methods

  • 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

State Access Methods

  • getCurrentPath() - Get current path
  • getSegments() - Get parsed segments
  • getParams() - Get route parameters
  • getQuery() - Get query parameters

Route Management Methods

  • addRoute(path, config) - Add a route
  • removeRoute(path) - Remove a route
  • hasRoute(path) - Check if route exists
  • matchRoute(path) - Match route pattern

Utility Methods

  • buildUrl(path, params, query) - Build URL string
  • parseUrl(url) - Parse URL components
  • isActive(path, exact) - Check if route is active

Troubleshooting

Common Issues

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.

Debug Mode

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.

License

MIT License - See the Juris framework documentation for full license details.

Clone this wiki locally