v1.0.0

🧭 Router System

OnigiriJS Router provides lightweight, PJAX-first routing for smoother page transitions without full reloads. Perfect for traditional server-rendered applications that want SPA-like navigation.

Quick Start

<script src="onigiri-core.js"></script>
<script src="onigiri-security.js"></script>
<script src="onigiri-router.js"></script>

<script>
// Initialize router
Onigiri.router.init({
    container: '#main-content',
    pjax: true,
    scrollToTop: true
});

// Define routes (optional - PJAX works without route definitions)
Onigiri.router.route('/about', (ctx) => {
    console.log('About page loaded');
});

// That's it! Links with data-route will now use PJAX
</script>

<!-- Links automatically use PJAX -->
<a href="/about" data-route>About</a>

Interactive Demo

PJAX Navigation

Click these links to see PJAX in action (content loads without full page reload):

πŸ™ Welcome to OnigiriJS Router Demo

Click the buttons above to see smooth page transitions.

Current route: /demo

Route Parameters

Routes can extract dynamic parameters from URLs:

Select a user to view their profile

Browser Navigation

Router Modes

History Mode (Default)

// Uses HTML5 History API for clean URLs
Onigiri.router.init({
    mode: 'history',  // Clean URLs: /about, /users/123
    root: '/'
});

// URLs look like:
// https://example.com/about
// https://example.com/users/123
// https://example.com/blog/my-post

Hash Mode

// Uses URL hash for routing (no server configuration needed)
Onigiri.router.init({
    mode: 'hash'  // Hash URLs: #/about, #/users/123
});

// URLs look like:
// https://example.com/#/about
// https://example.com/#/users/123
// https://example.com/#/blog/my-post

Configuration Options

Onigiri.router.init({
    mode: 'history',           // 'history' or 'hash'
    root: '/',                 // Base path for router
    container: '#main',        // Container for PJAX content
    linkSelector: 'a[data-route]',  // Links to intercept
    formSelector: 'form[data-route]', // Forms to intercept
    pjax: true,                // Enable PJAX
    pjaxTimeout: 5000,         // PJAX request timeout (ms)
    scrollToTop: true,         // Scroll to top on navigation
    scrollBehavior: 'smooth',  // 'smooth' or 'auto'
    updateTitle: true,         // Update document title from response
    csrf: true,                // Include CSRF token
    cachePages: true,          // Cache visited pages
    maxCache: 20,              // Maximum cached pages
    prefetch: false,           // Prefetch links on hover
    prefetchDelay: 100,        // Prefetch delay (ms)
    loadingClass: 'route-loading',  // Class added during load
    transitionDuration: 300    // Transition duration (ms)
});

Defining Routes

Basic Route

// Define a route handler
Onigiri.router.route('/about', (context) => {
    console.log('About page loaded');
    console.log('Path:', context.path);
});

Route with Parameters

// Extract parameters from URL
Onigiri.router.route('/users/:id', (context) => {
    const userId = context.params.id;
    console.log('User ID:', userId);
    
    // Load user data
    loadUserProfile(userId);
});

// Matches: /users/123, /users/456, etc.
// context.params.id = "123", "456", etc.

Multiple Parameters

// Multiple dynamic segments
Onigiri.router.route('/blog/:category/:slug', (context) => {
    console.log('Category:', context.params.category);
    console.log('Slug:', context.params.slug);
});

// Matches: /blog/tech/my-post
// context.params = { category: 'tech', slug: 'my-post' }

Wildcard Routes

// Catch-all route
Onigiri.router.route('/admin/*', (context) => {
    console.log('Admin section:', context.path);
});

// Matches: /admin/users, /admin/settings, /admin/anything

Named Routes

// Define named route
Onigiri.router.route('/profile/:username', profileHandler, {
    name: 'user.profile'
});

// Generate URL from name
const url = Onigiri.router.url('user.profile', { 
    username: 'john' 
});
console.log(url); // "/profile/john"

// Navigate to named route
Onigiri.navigate(url);

Bulk Route Registration

// Register multiple routes at once
Onigiri.router.route({
    '/': homeHandler,
    '/about': aboutHandler,
    '/contact': contactHandler,
    '/users/:id': userHandler,
    '/blog/:slug': blogHandler
});

Navigation

Programmatic Navigation

// Navigate to a path
Onigiri.router.navigate('/about');

// Navigate with options
Onigiri.router.navigate('/users/123', {
    trigger: true,     // Trigger route handler
    replace: false,    // Use pushState (not replaceState)
    data: { id: 123 }, // State data
    scroll: true       // Scroll to top
});

// Shorthand
Onigiri.navigate('/about');

Browser Navigation

// Go back
Onigiri.router.back();

// Go forward
Onigiri.router.forward();

// Reload current route
Onigiri.router.reload();

// Reload and bypass cache
Onigiri.router.reload(true);

Link Navigation

<!-- Links with data-route use PJAX -->
<a href="/about" data-route>About</a>
<a href="/users/123" data-route>User Profile</a>

<!-- Regular links (full page reload) -->
<a href="/logout">Logout</a>

Form Navigation

<!-- Forms with data-route submit via PJAX -->
<form action="/search" method="GET" data-route>
    <input type="text" name="q" placeholder="Search...">
    <button type="submit">Search</button>
</form>

Lifecycle Hooks

Before Navigation

// Run before every navigation
Onigiri.router.before((context) => {
    console.log('Navigating to:', context.path);
    console.log('From:', context.from);
    
    // Cancel navigation
    if (needsAuth && !isAuthenticated) {
        context.cancel = true;
        Onigiri.navigate('/login');
    }
});

After Navigation

// Run after every navigation
Onigiri.router.after((context) => {
    console.log('Navigation complete:', context.path);
    
    // Analytics
    trackPageView(context.path);
    
    // Update UI
    updateActiveNavLinks(context.path);
});

Error Handling

// Handle navigation errors
Onigiri.router.onError((context) => {
    console.error('Navigation error:', context.error);
    
    // Show error to user
    Onigiri.toast('Failed to load page', { icon: '❌' });
    
    context.handled = true; // Prevent default error handling
});

Middleware

Route Middleware

// Define middleware functions
const authMiddleware = (context, next) => {
    if (isAuthenticated()) {
        next(); // Continue to route handler
    } else {
        Onigiri.navigate('/login');
    }
};

const loggerMiddleware = (context, next) => {
    console.log('Route accessed:', context.path);
    next();
};

// Apply to specific route
Onigiri.router.route('/admin', adminHandler, {
    middleware: [authMiddleware, loggerMiddleware]
});

Chaining Middleware

const checkRole = (role) => {
    return (context, next) => {
        if (userHasRole(role)) {
            next();
        } else {
            Onigiri.navigate('/unauthorized');
        }
    };
};

Onigiri.router.route('/admin/users', usersHandler, {
    middleware: [
        authMiddleware,
        checkRole('admin'),
        loggerMiddleware
    ]
});

Advanced Features

Page Caching

// Enable caching (default: true)
Onigiri.router.init({
    cachePages: true,
    maxCache: 20  // Cache up to 20 pages
});

// Clear specific page cache
Onigiri.router.clearCache('/about');

// Clear all cache
Onigiri.router.clearCache();

// Disable cache for specific route
Onigiri.router.route('/fresh-data', handler, {
    cache: false
});

Link Prefetching

// Enable prefetching on hover
Onigiri.router.init({
    prefetch: true,
    prefetchDelay: 100  // Wait 100ms before prefetching
});

// Manual prefetch
Onigiri.router.prefetch('/about');

Query Parameters

Onigiri.router.route('/search', (context) => {
    // Access query parameters
    console.log(context.query);
    // URL: /search?q=onigiri&page=2
    // context.query = { q: 'onigiri', page: '2' }
    
    const searchTerm = context.query.q;
    const page = context.query.page || 1;
});

State Management

// Navigate with state
Onigiri.router.navigate('/users/123', {
    data: { 
        referrer: 'search',
        filters: { role: 'admin' }
    }
});

// Access state in route handler
Onigiri.router.route('/users/:id', (context) => {
    console.log(context.state);
    // { referrer: 'search', filters: { role: 'admin' } }
});

Get Current Route

// Get current path
const path = Onigiri.router.getCurrentPath();

// Get current route object
const route = Onigiri.router.getCurrentRoute();
console.log(route.path);
console.log(route.route);

Server-Side Configuration

PHP (Apache with .htaccess)

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteBase /
    
    # Redirect all requests to index.php
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^(.*)$ index.php?url=$1 [QSA,L]
</IfModule>

PHP (index.php)

<?php
// Get the requested path
$path = $_GET['url'] ?? '/';

// Your routing logic here
// Include appropriate template based on path
?>
<!DOCTYPE html>
<html>
<head>
    <title>My App</title>
</head>
<body>
    <nav>
        <a href="/" data-route>Home</a>
        <a href="/about" data-route>About</a>
        <a href="/contact" data-route>Contact</a>
    </nav>
    
    <main id="content">
        <?php include "pages/{$path}.php"; ?>
    </main>
    
    <script src="onigiri-core.js"></script>
    <script src="onigiri-router.js"></script>
    <script>
        Onigiri.router.init({
            container: '#content',
            pjax: true
        });
    </script>
</body>
</html>

Node.js/Express

const express = require('express');
const app = express();

// Serve static files
app.use(express.static('public'));

// Catch-all route
app.get('*', (req, res) => {
    res.sendFile(__dirname + '/public/index.html');
});

Real-World Examples

Forum Application

// Initialize router
Onigiri.router.init({
    container: '.main',
    pjax: true,
    scrollToTop: true,
    cachePages: true
});

// Define routes
Onigiri.router.route({
    '/': () => {
        console.log('Home page');
    },
    '/category/:slug': (ctx) => {
        loadCategory(ctx.params.slug);
    },
    '/topic/:slug': (ctx) => {
        loadTopic(ctx.params.slug);
        trackTopicView(ctx.params.slug);
    },
    '/profile/:username': (ctx) => {
        loadProfile(ctx.params.username);
    }
});

// Authentication middleware
const requireAuth = (ctx, next) => {
    if (currentUser) {
        next();
    } else {
        Onigiri.toast('Please login first', { icon: 'πŸ”’' });
        Onigiri.navigate('/login');
    }
};

// Protected routes
Onigiri.router.route('/admin', adminHandler, {
    middleware: [requireAuth]
});

// Track page views
Onigiri.router.after((ctx) => {
    analytics.track('pageview', { path: ctx.path });
});

E-commerce Site

// Product pages
Onigiri.router.route('/products/:id', async (context) => {
    const productId = context.params.id;
    
    try {
        const product = await loadProduct(productId);
        renderProduct(product);
    } catch (error) {
        Onigiri.toast('Product not found', { icon: '❌' });
        Onigiri.navigate('/products');
    }
});

// Shopping cart
Onigiri.router.route('/cart', (context) => {
    if (cart.isEmpty()) {
        Onigiri.toast('Your cart is empty', { icon: 'πŸ›’' });
        Onigiri.navigate('/products');
        return;
    }
    renderCart();
});

// Checkout with auth
Onigiri.router.route('/checkout', checkoutHandler, {
    middleware: [requireAuth, validateCart]
});

Dashboard with Tabs

// Tab navigation
Onigiri.router.route('/dashboard/:tab?', (context) => {
    const tab = context.params.tab || 'overview';
    
    // Update active tab
    O('.tab').removeClass('active');
    O(\`[data-tab="\${tab}"]\`).addClass('active');
    
    // Load tab content
    loadDashboardTab(tab);
});

// Links update URL without reload
O('.tab-link').on('click', function(e) {
    e.preventDefault();
    const tab = this.dataset.tab;
    Onigiri.navigate(\`/dashboard/\${tab}\`);
});

Router Events

// Listen for router events
document.addEventListener('onigiri:router:ready', () => {
    console.log('Router initialized');
});

document.addEventListener('onigiri:router:beforeLoad', (e) => {
    console.log('Loading:', e.detail.path);
    showLoadingBar();
});

document.addEventListener('onigiri:router:complete', (e) => {
    console.log('Loaded:', e.detail.path);
    hideLoadingBar();
});
πŸ™ Router Best Practices:
  • Use PJAX for content-heavy sites to improve perceived performance
  • Enable caching for frequently visited pages
  • Add loading indicators during route transitions
  • Use middleware for authentication and authorization
  • Handle errors gracefully with error hooks
  • Configure server to handle HTML5 History API
  • Test with JavaScript disabled (graceful degradation)

Configuration Reference

Option Type Default Description
mode string 'history' Routing mode: 'history' or 'hash'
root string '/' Base path for router
container string '#main' Container selector for PJAX
pjax boolean true Enable PJAX navigation
scrollToTop boolean true Scroll to top on navigation
cachePages boolean true Cache visited pages
maxCache number 20 Maximum cached pages
prefetch boolean false Prefetch links on hover
updateTitle boolean true Update document title

βœ… Development Roadmap

Track the progress of OnigiriJS modules. Tasks are marked complete by the development team.

OnigiriJS Module Roadmap

Implementation progress of planned modules

6 / 21 completed (29%)
onigiri-state
Shared global & scoped state management
onigiri-directives
Declarative DOM bindings (o-show, o-model, etc.)
onigiri-resource
REST-style data models over AJAX
onigiri-observe
Intersection & Mutation observer helpers
onigiri-humhub-ui
Standard HumHub UI abstractions (modal, notify, confirm)
onigiri-lifecycle
Component lifecycle hooks
onigiri-guard
Debounce, throttle, single-run guards
onigiri-scroll
Scroll save/restore & helpers (PJAX-friendly)
onigiri-permission
Client-side permission awareness
onigiri-portal
DOM teleport / overlay mounting
onigiri-router
Micro router (non-SPA, PJAX-first)
onigiri-sanitize
HTML & input sanitization
onigiri-shortcut
Keyboard shortcut manager
onigiri-queue
Sequential async task runner
onigiri-gesture
Touch & swipe helpers
onigiri-devtools
Debugging & inspection helpers
onigiri-plugin
Plugin registration system
onigiri-time
Relative time & timezone utilities
onigiri-emojis
Emoji Picker and Manager
onigiri-tasks
Task Management
onigiri-polls
Polls creation and management
Note: Task completion is managed by the OnigiriJS development team.