# Routing API 🌐 SigPro includes a simple yet powerful hash-based router designed for Single Page Applications (SPAs). It works everywhere with zero server configuration and integrates seamlessly with `$.page` for automatic cleanup. ## Why Hash-Based Routing? Hash routing (`#/about`) works **everywhere** - no server configuration needed. Perfect for: - Static sites and SPAs - GitHub Pages, Netlify, any static hosting - Local development without a server - Projects that need to work immediately ## `$.router(routes)` Creates a hash-based router that renders the matching component and handles navigation. ```javascript import { $, html } from 'sigpro'; import HomePage from './pages/Home.js'; import AboutPage from './pages/About.js'; import UserPage from './pages/User.js'; const routes = [ { path: '/', component: HomePage }, { path: '/about', component: AboutPage }, { path: '/user/:id', component: UserPage }, ]; // Mount the router document.body.appendChild($.router(routes)); ``` ## 📋 API Reference ### `$.router(routes)` | Parameter | Type | Description | |-----------|------|-------------| | `routes` | `Array` | Array of route configurations | **Returns:** `HTMLDivElement` - Container that renders the current page ### `$.router.go(path)` | Parameter | Type | Description | |-----------|------|-------------| | `path` | `string` | Route path to navigate to (automatically adds leading slash) | ### Route Object | Property | Type | Description | |----------|------|-------------| | `path` | `string` or `RegExp` | Route pattern to match | | `component` | `Function` | Function that returns page content (receives `params`) | ## 🎯 Route Patterns ### String Paths (Simple Routes) ```javascript const routes = [ // Static routes { path: '/', component: HomePage }, { path: '/about', component: AboutPage }, { path: '/contact', component: ContactPage }, // Routes with parameters { path: '/user/:id', component: UserPage }, { path: '/user/:id/posts', component: UserPostsPage }, { path: '/user/:id/posts/:postId', component: PostPage }, { path: '/search/:query/page/:num', component: SearchPage }, ]; ``` ### RegExp Paths (Advanced Routing) ```javascript const routes = [ // Match numeric IDs only { path: /^\/users\/(?\d+)$/, component: UserPage }, // Match product slugs (letters, numbers, hyphens) { path: /^\/products\/(?[a-z0-9-]+)$/, component: ProductPage }, // Match blog posts by year/month { path: /^\/blog\/(?\d{4})\/(?\d{2})$/, component: BlogArchive }, // Match optional language prefix { path: /^\/(?en|es|fr)?\/?about$/, component: AboutPage }, // Match UUID format { path: /^\/items\/(?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/, component: ItemPage }, ]; ``` ## 📦 Basic Examples ### Simple Router Setup ```javascript // main.js import { $, html } from 'sigpro'; import Home from './pages/Home.js'; import About from './pages/About.js'; import Contact from './pages/Contact.js'; const routes = [ { path: '/', component: Home }, { path: '/about', component: About }, { path: '/contact', component: Contact }, ]; const router = $.router(routes); // Mount to DOM document.body.appendChild(router); ``` ### Page Components with Parameters ```javascript // pages/User.js import { $, html } from 'sigpro'; export default (params) => $.page(() => { // /user/42 → params = { id: '42' } // /user/john/posts/123 → params = { id: 'john', postId: '123' } const userId = params.id; const userData = $(null); $.effect(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => userData(data)); }); return html`

User Profile: ${userId}

${() => userData() ? html`

Name: ${userData().name}

Email: ${userData().email}

` : html`

Loading...

`}
`; }); ``` ### Navigation ```javascript import { $, html } from 'sigpro'; // In templates const NavBar = () => html` `; ``` ## 🚀 Advanced Examples ### Complete Application with Layout ```javascript // App.js import { $, html } from 'sigpro'; import HomePage from './pages/Home.js'; import AboutPage from './pages/About.js'; import UserPage from './pages/User.js'; import SettingsPage from './pages/Settings.js'; import NotFound from './pages/NotFound.js'; // Layout component with navigation const Layout = (content) => html` `; // Helper to check active route const isActive = (path) => { const current = window.location.hash.replace(/^#/, '') || '/'; return current === path; }; // Routes with layout const routes = [ { path: '/', component: (params) => Layout(HomePage(params)) }, { path: '/about', component: (params) => Layout(AboutPage(params)) }, { path: '/user/:id', component: (params) => Layout(UserPage(params)) }, { path: '/settings', component: (params) => Layout(SettingsPage(params)) }, { path: '/:path(.*)', component: (params) => Layout(NotFound(params)) }, // Catch-all ]; // Create and mount router const router = $.router(routes); document.body.appendChild(router); ``` ### Nested Routes ```javascript // pages/Settings.js (parent route) import { $, html } from 'sigpro'; import SettingsGeneral from './settings/General.js'; import SettingsSecurity from './settings/Security.js'; import SettingsNotifications from './settings/Notifications.js'; export default (params) => $.page(() => { const section = params.section || 'general'; const sections = { general: SettingsGeneral, security: SettingsSecurity, notifications: SettingsNotifications }; const CurrentSection = sections[section]; return html` `; }); // pages/settings/General.js export default (params) => $.page(() => { return html`

General Settings

...
`; }); // Main router with nested routes const routes = [ { path: '/', component: HomePage }, { path: '/settings/:section?', component: SettingsPage }, // Optional section param ]; ``` ### Protected Routes (Authentication) ```javascript // auth.js import { $ } from 'sigpro'; const isAuthenticated = $(false); const user = $(null); export const checkAuth = async () => { const token = localStorage.getItem('token'); if (token) { try { const response = await fetch('/api/verify'); if (response.ok) { const userData = await response.json(); user(userData); isAuthenticated(true); return true; } } catch (e) { // Handle error } } isAuthenticated(false); user(null); return false; }; export const requireAuth = (component) => (params) => { if (isAuthenticated()) { return component(params); } // Redirect to login $.router.go('/login'); return null; }; export { isAuthenticated, user }; ``` ```javascript // pages/Dashboard.js (protected route) import { $, html } from 'sigpro'; import { requireAuth, user } from '../auth.js'; const Dashboard = (params) => $.page(() => { return html`

Welcome, ${() => user()?.name}!

This is your protected dashboard.

`; }); export default requireAuth(Dashboard); ``` ```javascript // main.js with protected routes import { $, html } from 'sigpro'; import { checkAuth } from './auth.js'; import HomePage from './pages/Home.js'; import LoginPage from './pages/Login.js'; import DashboardPage from './pages/Dashboard.js'; import AdminPage from './pages/Admin.js'; // Check auth on startup checkAuth(); const routes = [ { path: '/', component: HomePage }, { path: '/login', component: LoginPage }, { path: '/dashboard', component: DashboardPage }, // Protected { path: '/admin', component: AdminPage }, // Protected ]; document.body.appendChild($.router(routes)); ``` ### Route Transitions ```javascript // with-transitions.js import { $, html } from 'sigpro'; export const createRouterWithTransitions = (routes) => { const transitioning = $(false); const currentView = $(null); const nextView = $(null); const container = document.createElement('div'); container.style.display = 'contents'; const renderWithTransition = async (newView) => { if (currentView() === newView) return; transitioning(true); nextView(newView); // Fade out container.style.transition = 'opacity 0.2s'; container.style.opacity = '0'; await new Promise(resolve => setTimeout(resolve, 200)); // Update content container.replaceChildren(newView); currentView(newView); // Fade in container.style.opacity = '1'; await new Promise(resolve => setTimeout(resolve, 200)); transitioning(false); container.style.transition = ''; }; const router = $.router(routes.map(route => ({ ...route, component: (params) => { const view = route.component(params); renderWithTransition(view); return document.createComment('router-placeholder'); } }))); return router; }; ``` ### Breadcrumbs Navigation ```javascript // with-breadcrumbs.js import { $, html } from 'sigpro'; export const createBreadcrumbs = (routes) => { const breadcrumbs = $([]); const updateBreadcrumbs = (path) => { const parts = path.split('/').filter(Boolean); const crumbs = []; let currentPath = ''; parts.forEach((part, index) => { currentPath += `/${part}`; // Find matching route const route = routes.find(r => { if (r.path.includes(':')) { const pattern = r.path.replace(/:[^/]+/g, part); return pattern === currentPath; } return r.path === currentPath; }); crumbs.push({ path: currentPath, label: route?.name || part.charAt(0).toUpperCase() + part.slice(1), isLast: index === parts.length - 1 }); }); breadcrumbs(crumbs); }; // Listen to route changes window.addEventListener('hashchange', () => { const path = window.location.hash.replace(/^#/, '') || '/'; updateBreadcrumbs(path); }); // Initial update updateBreadcrumbs(window.location.hash.replace(/^#/, '') || '/'); return breadcrumbs; }; ``` ```javascript // Usage in layout import { createBreadcrumbs } from './with-breadcrumbs.js'; const breadcrumbs = createBreadcrumbs(routes); const Layout = (content) => html`
${content}
`; ``` ### Query Parameters ```javascript // with-query-params.js export const getQueryParams = () => { const hash = window.location.hash; const queryStart = hash.indexOf('?'); if (queryStart === -1) return {}; const queryString = hash.slice(queryStart + 1); const params = new URLSearchParams(queryString); const result = {}; for (const [key, value] of params) { result[key] = value; } return result; }; export const updateQueryParams = (params) => { const hash = window.location.hash.split('?')[0]; const queryString = new URLSearchParams(params).toString(); window.location.hash = queryString ? `${hash}?${queryString}` : hash; }; ``` ```javascript // Search page with query params import { $, html } from 'sigpro'; import { getQueryParams, updateQueryParams } from './with-query-params.js'; export default (params) => $.page(() => { // Get initial query from URL const queryParams = getQueryParams(); const searchQuery = $(queryParams.q || ''); const page = $(parseInt(queryParams.page) || 1); const results = $([]); // Update URL when search changes $.effect(() => { updateQueryParams({ q: searchQuery() || undefined, page: page() > 1 ? page() : undefined }); }); // Fetch results when search or page changes $.effect(() => { if (searchQuery()) { fetch(`/api/search?q=${searchQuery()}&page=${page()}`) .then(res => res.json()) .then(data => results(data)); } }); return html`

Search

{ searchQuery(e.target.value); page(1); // Reset to first page on new search }} />
${results().map(item => html`
${item.title}
`)}
${() => results().length ? html` ` : ''}
`; }); ``` ### Lazy Loading Routes ```javascript // lazy.js export const lazy = (loader) => { let component = null; return async (params) => { if (!component) { const module = await loader(); component = module.default; } return component(params); }; }; ``` ```javascript // main.js with lazy loading import { $, html } from 'sigpro'; import { lazy } from './lazy.js'; import Layout from './Layout.js'; const routes = [ { path: '/', component: lazy(() => import('./pages/Home.js')) }, { path: '/about', component: lazy(() => import('./pages/About.js')) }, { path: '/dashboard', component: lazy(() => import('./pages/Dashboard.js')) }, { path: '/admin', component: lazy(() => import('./pages/Admin.js')), // Show loading state loading: () => html`
Loading admin panel...
` }, ]; // Wrap with layout const routesWithLayout = routes.map(route => ({ ...route, component: (params) => Layout(route.component(params)) })); document.body.appendChild($.router(routesWithLayout)); ``` ### Route Guards / Middleware ```javascript // middleware.js export const withGuard = (component, guard) => (params) => { const result = guard(params); if (result === true) { return component(params); } else if (typeof result === 'string') { $.router.go(result); return null; } return result; // Custom component (e.g., AccessDenied) }; // Guards export const roleGuard = (requiredRole) => (params) => { const userRole = localStorage.getItem('userRole'); if (userRole === requiredRole) return true; if (!userRole) return '/login'; return AccessDeniedPage(params); }; export const authGuard = () => (params) => { const token = localStorage.getItem('token'); return token ? true : '/login'; }; export const pendingChangesGuard = (hasPendingChanges) => (params) => { if (hasPendingChanges()) { return ConfirmLeavePage(params); } return true; }; ``` ```javascript // Usage import { withGuard, authGuard, roleGuard } from './middleware.js'; const routes = [ { path: '/', component: HomePage }, { path: '/profile', component: withGuard(ProfilePage, authGuard()) }, { path: '/admin', component: withGuard(AdminPage, roleGuard('admin')) }, ]; ``` ## 📊 Route Matching Priority Routes are matched in the order they are defined. More specific routes should come first: ```javascript const routes = [ // More specific first { path: '/user/:id/edit', component: EditUserPage }, { path: '/user/:id/posts', component: UserPostsPage }, { path: '/user/:id', component: UserPage }, // Static routes { path: '/about', component: AboutPage }, { path: '/contact', component: ContactPage }, // Catch-all last { path: '/:path(.*)', component: NotFoundPage }, ]; ``` ## 🎯 Complete Example ```javascript // main.js - Complete application import { $, html } from 'sigpro'; import { lazy } from './utils/lazy.js'; import { withGuard, authGuard } from './utils/middleware.js'; import Layout from './components/Layout.js'; // Lazy load pages const HomePage = lazy(() => import('./pages/Home.js')); const AboutPage = lazy(() => import('./pages/About.js')); const LoginPage = lazy(() => import('./pages/Login.js')); const DashboardPage = lazy(() => import('./pages/Dashboard.js')); const UserPage = lazy(() => import('./pages/User.js')); const SettingsPage = lazy(() => import('./pages/Settings.js')); const NotFoundPage = lazy(() => import('./pages/NotFound.js')); // Route configuration const routes = [ { path: '/', component: HomePage, name: 'Home' }, { path: '/about', component: AboutPage, name: 'About' }, { path: '/login', component: LoginPage, name: 'Login' }, { path: '/dashboard', component: withGuard(DashboardPage, authGuard()), name: 'Dashboard' }, { path: '/user/:id', component: UserPage, name: 'User Profile' }, { path: '/settings/:section?', component: withGuard(SettingsPage, authGuard()), name: 'Settings' }, { path: '/:path(.*)', component: NotFoundPage, name: 'Not Found' }, ]; // Wrap all routes with layout const routesWithLayout = routes.map(route => ({ ...route, component: (params) => Layout(route.component(params)) })); // Create and mount router const router = $.router(routesWithLayout); document.body.appendChild(router); // Navigation helper (available globally) window.navigate = $.router.go; ``` ## 📊 Summary | Feature | Description | |---------|-------------| | **Hash-based** | Works everywhere, no server config | | **Route Parameters** | `:param` syntax for dynamic segments | | **RegExp Support** | Advanced pattern matching | | **Query Parameters** | Support for `?key=value` in URLs | | **Programmatic Navigation** | `$.router.go(path)` | | **Auto-cleanup** | Works with `$.page` for memory management | | **Zero Dependencies** | Pure vanilla JavaScript | | **Lazy Loading Ready** | Easy code splitting | --- > **Pro Tip:** Order matters in route definitions - put more specific routes (with parameters) before static ones, and always include a catch-all route (404) at the end.