# Routing API 🌐 | SigPro
Minimalist Reactive Library

Skip to content

# 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<Route>` | 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\/(?<id>\d+)$/, component: UserPage },

      // Match product slugs (letters, numbers, hyphens)
      { path: /^\/products\/(?<slug>[a-z0-9-]+)$/, component: ProductPage },

      // Match blog posts by year/month
      { path: /^\/blog\/(?<year>\d{4})\/(?<month>\d{2})$/, component: BlogArchive },

      // Match optional language prefix
      { path: /^\/(?<lang>en|es|fr)?\/?about$/, component: AboutPage },

      // Match UUID format
      { path: /^\/items\/(?<uuid>[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`
        <div class="user-page">
          <h1>User Profile: ${userId}</h1>
          ${() => userData() ? html`
            <p>Name: ${userData().name}</p>
            <p>Email: ${userData().email}</p>
          ` : html`<p>Loading...</p>`}
        </div>
      `;
    });

### Navigation ​

javascript

    import { $, html } from 'sigpro';

    // In templates
    const NavBar = () => html`
      <nav>
        <a href="#/">Home</a>
        <a href="#/about">About</a>
        <a href="#/contact">Contact</a>
        <a href="#/user/42">Profile</a>
        <a href="#/search/js/page/1">Search</a>

        <!-- Programmatic navigation -->
        <button @click=${() => $.router.go('/about')}>
          Go to About
        </button>

        <button @click=${() => $.router.go('contact')}>
          Go to Contact (auto-adds leading slash)
        </button>
      </nav>
    `;

## 🚀 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`
      <div class="app">
        <header class="header">
          <h1>My SigPro App</h1>
          <nav class="nav">
            <a href="#/" class:active=${() => isActive('/')}>Home</a>
            <a href="#/about" class:active=${() => isActive('/about')}>About</a>
            <a href="#/user/42" class:active=${() => isActive('/user/42')}>Profile</a>
            <a href="#/settings" class:active=${() => isActive('/settings')}>Settings</a>
          </nav>
        </header>

        <main class="main">
          ${content}
        </main>

        <footer class="footer">
          <p>© 2024 SigPro App</p>
        </footer>
      </div>
    `;

    // 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`
        <div class="settings">
          <h1>Settings</h1>

          <div class="settings-layout">
            <nav class="settings-sidebar">
              <a href="#/settings/general" class:active=${() => section === 'general'}>
                General
              </a>
              <a href="#/settings/security" class:active=${() => section === 'security'}>
                Security
              </a>
              <a href="#/settings/notifications" class:active=${() => section === 'notifications'}>
                Notifications
              </a>
            </nav>

            <div class="settings-content">
              ${CurrentSection(params)}
            </div>
          </div>
        </div>
      `;
    });

    // pages/settings/General.js
    export default (params) => $.page(() => {
      return html`
        <div>
          <h2>General Settings</h2>
          <form>...</form>
        </div>
      `;
    });

    // 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`
        <div class="dashboard">
          <h1>Welcome, ${() => user()?.name}!</h1>
          <p>This is your protected dashboard.</p>
        </div>
      `;
    });

    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`
      <div class="app">
        <nav class="breadcrumbs">
          ${() => breadcrumbs().map(crumb => html`
            ${!crumb.isLast ? html`
              <a href="#${crumb.path}">${crumb.label}</a>
              <span class="separator">/</span>
            ` : html`
              <span class="current">${crumb.label}</span>
            `}
          `)}
        </nav>

        <main>
          ${content}
        </main>
      </div>
    `;

### 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`
        <div class="search-page">
          <h1>Search</h1>

          <input
            type="search"
            :value=${searchQuery}
            placeholder="Search..."
            @input=${(e) => {
              searchQuery(e.target.value);
              page(1); // Reset to first page on new search
            }}
          />

          <div class="results">
            ${results().map(item => html`
              <div class="result">${item.title}</div>
            `)}
          </div>

          ${() => results().length ? html`
            <div class="pagination">
              <button
                ?disabled=${() => page() <= 1}
                @click=${() => page(p => p - 1)}
              >
                Previous
              </button>

              <span>Page ${page}</span>

              <button
                ?disabled=${() => results().length < 10}
                @click=${() => page(p => p + 1)}
              >
                Next
              </button>
            </div>
          ` : ''}
        </div>
      `;
    });

### 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`<div class="loading">Loading admin panel...</div>`
      },
    ];

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