v1.0.0

πŸš€ PJAX Support

PJAX (pushState + AJAX) provides seamless page transitions without full page reloads, making your application feel fast and smooth like a single-page app.

What is PJAX?

PJAX combines HTML5 pushState with AJAX to create fast, smooth page transitions:

  • βœ… No full page reload
  • βœ… Preserves scroll position
  • βœ… Updates browser history
  • βœ… Works with browser back/forward
  • βœ… Caches responses for instant returns
  • βœ… Graceful fallback to normal navigation

Initialization

Basic Setup

// Initialize PJAX
Onigiri.pjax.init();

// That's it! Links with data-pjax now use PJAX

With Configuration

Onigiri.pjax.init({
    timeout: 5000,          // Request timeout (ms)
    push: true,             // Add to history
    replace: false,         // Replace history instead
    scrollTo: 0,            // Scroll position after load
    maxCacheLength: 20,     // Cache size
    csrf: true              // Include CSRF token
});

HTML Markup

PJAX Container

<!-- Define container for dynamic content -->
<div id="pjax-container">
    <!-- Content will be loaded here -->
    <h1>Home Page</h1>
    <p>Welcome to OnigiriJS! πŸ™</p>
</div>

PJAX Links

<!-- Add data-pjax attribute to enable PJAX -->
<a href="/about" data-pjax="#pjax-container">
    About
</a>

<a href="/menu" data-pjax="#pjax-container">
    Menu
</a>

<a href="/contact" data-pjax="#pjax-container">
    Contact
</a>

<!-- Links without data-pjax work normally -->
<a href="/external">External Link</a>

PJAX Forms

<form action="/search" method="GET" data-pjax="#pjax-container">
    <input type="text" name="q" placeholder="Search...">
    <button type="submit">Search πŸ™</button>
</form>

<form action="/submit" method="POST" data-pjax="#pjax-container">
    <input type="text" name="name">
    <button type="submit">Submit</button>
</form>

Programmatic Loading

Load URL

// Load page via PJAX
Onigiri.pjax.load('/about', {
    container: '#pjax-container',
    push: true,
    scrollTo: 0
});

// Load with custom options
Onigiri.pjax.load('/menu', {
    container: '#content',
    push: true,
    nocache: true,  // Skip cache
    scrollTo: 100
});

Submit Form

// Submit form via PJAX
const form = document.querySelector('#search-form');
Onigiri.pjax.submit(form, {
    container: '#results'
});

Events

Before PJAX Load

// Listen before PJAX request
document.addEventListener('onigiri:pjax:before', (e) => {
    console.log('πŸ™ Loading:', e.detail.url);
    
    // Show loading indicator
    O('#loading-indicator').show();
    
    // You can cancel the request
    // e.preventDefault();
    // return false;
});

After PJAX Complete

// Listen after PJAX completes
document.addEventListener('onigiri:pjax:complete', (e) => {
    console.log('βœ… Loaded:', e.detail.url);
    
    // Hide loading indicator
    O('#loading-indicator').hide();
    
    // Re-initialize components
    initializeWidgets();
    
    // Update analytics
    trackPageView(e.detail.url);
    
    // Highlight code blocks
    highlightCode();
});

Cache Management

Clear Specific URL

// Clear cached page
Onigiri.pjax.clearCache('/about');

// Clear multiple pages
['/about', '/contact', '/menu'].forEach(url => {
    Onigiri.pjax.clearCache(url);
});

Clear All Cache

// Clear entire PJAX cache
Onigiri.pjax.clearCache();

// Useful on logout or data update
O('#logout-btn').on('click', () => {
    Onigiri.pjax.clearCache();
    logout();
});

Skip Cache

// Force fresh load (skip cache)
Onigiri.pjax.load('/fresh-data', {
    container: '#content',
    nocache: true
});

Complete Example

HTML Structure

<!DOCTYPE html>
<html>
<head>
    <title>OnigiriJS PJAX Demo</title>
    <meta name="csrf-token" content="<?= csrf_token() ?>">
</head>
<body>
    <!-- Navigation -->
    <nav id="main-nav">
        <a href="/" data-pjax="#pjax-container">Home</a>
        <a href="/about" data-pjax="#pjax-container">About</a>
        <a href="/menu" data-pjax="#pjax-container">Menu</a>
        <a href="/contact" data-pjax="#pjax-container">Contact</a>
    </nav>
    
    <!-- Loading Indicator -->
    <div id="loading-bar" style="display:none;">
        Loading... πŸ™
    </div>
    
    <!-- PJAX Container -->
    <main id="pjax-container">
        <h1>Home Page</h1>
        <p>Welcome to our onigiri shop! πŸ™</p>
    </main>
    
    <!-- Footer (not replaced by PJAX) -->
    <footer>
        <p>© 2024 OnigiriJS</p>
    </footer>
    
    <script src="onigiri.core.js"></script>
    <script src="onigiri.security.js"></script>
    <script src="onigiri.pjax.js"></script>
    <script src="app.js"></script>
</body>
</html>

JavaScript Setup

// app.js
(function() {
    // Initialize security
    Onigiri.security.init();
    
    // Initialize PJAX
    Onigiri.pjax.init({
        timeout: 5000,
        scrollTo: 0,
        maxCacheLength: 20
    });
    
    // Show loading bar
    document.addEventListener('onigiri:pjax:before', (e) => {
        O('#loading-bar').show();
        O('#pjax-container').addClass('loading');
    });
    
    // Hide loading bar and update UI
    document.addEventListener('onigiri:pjax:complete', (e) => {
        O('#loading-bar').hide();
        O('#pjax-container').removeClass('loading');
        
        // Update active nav link
        updateActiveNav(e.detail.url);
        
        // Re-initialize components
        initializePageComponents();
        
        // Track page view
        console.log('πŸ“Š Page view:', e.detail.url);
    });
    
    function updateActiveNav(url) {
        O('#main-nav a').removeClass('active');
        O(\`#main-nav a[href="\${url}"]\`).addClass('active');
    }
    
    function initializePageComponents() {
        // Re-initialize any components in new content
        O('.tooltip').each(function(el) {
            initTooltip(el);
        });
        
        O('.carousel').each(function(el) {
            initCarousel(el);
        });
    }
    
    console.log('πŸ™ PJAX ready!');
})();

Server-Side Setup

PHP Backend

<?php
// Detect PJAX request
$isPjax = isset($_SERVER['HTTP_X_PJAX']);

if ($isPjax) {
    // Return only the container content
    $container = $_SERVER['HTTP_X_PJAX_CONTAINER'] ?? '#pjax-container';
    
    // Just render the content
    include 'content/' . $page . '.php';
    exit;
}

// Full page render for non-PJAX requests
include 'layout/header.php';
include 'content/' . $page . '.php';
include 'layout/footer.php';
?>

Route Example

<?php
// routes.php
$router->get('/about', function() {
    $isPjax = isset($_SERVER['HTTP_X_PJAX']);
    
    if ($isPjax) {
        // Return just content
        return render('about', ['layout' => false]);
    }
    
    // Return full page
    return render('about', ['layout' => 'main']);
});
?>

Advanced Patterns

Multiple PJAX Containers

<!-- Sidebar -->
<aside id="sidebar">
    <a href="/cat/1" data-pjax="#main-content">Category 1</a>
    <a href="/cat/2" data-pjax="#main-content">Category 2</a>
</aside>

<!-- Main Content -->
<main id="main-content">
    <!-- Content loaded here -->
</main>

<!-- Detail Panel -->
<div id="detail-panel">
    <!-- Details loaded here -->
</div>

<script>
// Load into different containers
O('.category-link').on('click', function(e) {
    e.preventDefault();
    Onigiri.pjax.load(this.href, {
        container: '#main-content'
    });
});

O('.detail-link').on('click', function(e) {
    e.preventDefault();
    Onigiri.pjax.load(this.href, {
        container: '#detail-panel'
    });
});
</script>

Progressive Enhancement

// Works without PJAX if browser doesn't support it
<nav>
    <a href="/about" data-pjax="#content">About</a>
</nav>

<script>
// Check if PJAX is supported
if (typeof Onigiri !== 'undefined' && Onigiri.pjax) {
    Onigiri.pjax.init();
    console.log('πŸ™ PJAX enabled');
} else {
    console.log('Regular navigation');
}
</script>

Modal with PJAX

// Load content into modal
O('.modal-trigger').on('click', async function(e) {
    e.preventDefault();
    
    const url = this.href;
    
    // Show modal
    O('#modal').show();
    O('#modal-content').html('Loading... πŸ™');
    
    try {
        // Load content via PJAX
        await Onigiri.pjax.load(url, {
            container: '#modal-content',
            push: false,  // Don't update history
            scrollTo: false
        });
    } catch (error) {
        O('#modal-content').html('Failed to load content');
    }
});

// Close modal
O('#modal-close').on('click', () => {
    O('#modal').hide();
});

Infinite Scroll with PJAX

let currentPage = 1;
let loading = false;

// Detect scroll to bottom
window.addEventListener('scroll', () => {
    if (loading) return;
    
    const scrollHeight = document.documentElement.scrollHeight;
    const scrollTop = window.scrollY;
    const clientHeight = window.innerHeight;
    
    if (scrollTop + clientHeight >= scrollHeight - 100) {
        loadNextPage();
    }
});

async function loadNextPage() {
    loading = true;
    currentPage++;
    
    O('#loading-more').show();
    
    try {
        const response = await fetch(\`/api/items?page=\${currentPage}\`, {
            headers: { 'X-PJAX': 'true' }
        });
        
        const html = await response.text();
        
        // Append to existing content
        O('#items-container').append(html);
        
    } catch (error) {
        console.error('Failed to load more:', error);
    } finally {
        O('#loading-more').hide();
        loading = false;
    }
}

Tab System with PJAX

<div class="tabs">
    <button class="tab active" data-url="/tab/overview">Overview</button>
    <button class="tab" data-url="/tab/details">Details</button>
    <button class="tab" data-url="/tab/history">History</button>
</div>

<div id="tab-content">
    <!-- Tab content loaded here -->
</div>

<script>
O('.tab').on('click', function() {
    const url = O(this).data('url');
    
    // Update active tab
    O('.tab').removeClass('active');
    O(this).addClass('active');
    
    // Load content
    Onigiri.pjax.load(url, {
        container: '#tab-content',
        push: false,  // Don't update URL
        scrollTo: false
    });
});
</script>

Search with PJAX

<form id="search-form" data-pjax="#search-results">
    <input type="text" name="q" id="search-input">
    <button type="submit">Search πŸ™</button>
</form>

<div id="search-results">
    <!-- Results appear here -->
</div>

<script>
// Live search with debounce
const debouncedSearch = Onigiri.debounce(() => {
    const query = O('#search-input').val();
    
    if (query.length < 2) return;
    
    Onigiri.pjax.load(\`/search?q=\${encodeURIComponent(query)}\`, {
        container: '#search-results',
        push: false
    });
}, 300);

O('#search-input').on('input', debouncedSearch);
</script>

Browser Compatibility

// Check for required features
function isPjaxSupported() {
    return !!(
        window.history &&
        window.history.pushState &&
        window.XMLHttpRequest &&
        window.addEventListener
    );
}

if (isPjaxSupported()) {
    Onigiri.pjax.init();
} else {
    console.log('PJAX not supported, using standard navigation');
}
πŸ™ Pro Tip: PJAX works best when your server can return just the content fragment for PJAX requests and full pages for normal requests!

PJAX Best Practices

  • Always have a fallback for non-PJAX browsers
  • Use meaningful container IDs
  • Re-initialize JavaScript on PJAX complete
  • Clear cache on data updates
  • Show loading indicators
  • Handle errors gracefully (fall back to normal navigation)
  • Don't use PJAX for external links
  • Test browser back/forward buttons
  • Update page titles after PJAX load
  • Consider SEO implications

Troubleshooting

Scripts Not Running

// Scripts in PJAX content don't run by default
// Solution: Re-execute or use event listeners

document.addEventListener('onigiri:pjax:complete', () => {
    // Re-initialize scripts
    initializeComponents();
});

Event Handlers Lost

// Use event delegation for dynamic content
O(document).on('click', '.dynamic-button', function() {
    // This works even after PJAX load
});

// Instead of:
O('.dynamic-button').on('click', function() {
    // This won't work after PJAX
});

CSS Not Loading

// CSS should be in the main layout, not PJAX content
// Or load CSS before PJAX content

βœ… 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.