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