Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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

AltStyle によって変換されたページ (->オリジナル) /