Remove unused blade settings pages not linked from UI Convert remaining frontend pages to SPA actions Convert settings user_settings and general to SPA actions Convert settings profile pages to SPA actions Convert contacts and projects add/edit pages to SPA actions Convert clients add/edit page to SPA action with loading pattern Refactor component scoped IDs from $id to $sid Fix jqhtml comment syntax and implement universal error component system Update all application code to use new unified error system Remove all backwards compatibility - unified error system complete Phase 5: Remove old response classes Phase 3-4: Ajax response handler sends new format, old helpers deprecated Phase 2: Add client-side unified error foundation Phase 1: Add server-side unified error foundation Add unified Ajax error response system with constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.5 KiB
Executable File
9.5 KiB
Executable File
JQHTML SPA Router System - LLM Reference
Core Architecture
- Static router class
Jqhtml_Routermanages all navigation - Routes are ES6 classes extending
Jqhtml_Route - Layouts are ES6 classes extending
Jqhtml_Layout - SPA container is ES6 class extending
Jqhtml_SPA - Hash routing (
#/path) automatically enabled forfile://protocol - Standard routing for
http://andhttps://protocols
Route Definition
Basic Route Class
class HomeRoute extends Jqhtml_Route {
static routes = ['/']; // URL patterns this route handles
static layout = 'MainLayout'; // Layout to use (optional)
static meta = { name: 'home', requiresAuth: false }; // Metadata
async on_render() {
// Route component render logic
}
async pre_dispatch() {
// Return false to cancel navigation
// Return string URL to redirect
return true;
}
async post_dispatch() {
// Called after route fully loaded
}
}
Route Patterns
static routes = [
'/', // Exact match
'/users', // Static path
'/users/:id', // Parameter capture
'/posts/:id/comments', // Multiple segments
'/admin/:section/:id' // Multiple parameters
];
Parameter Access
class UserRoute extends Jqhtml_Route {
static routes = ['/users/:id'];
async on_load() {
// URL: /users/123?tab=profile
const userId = this.args.id; // '123' from URL pattern
const tab = this.args.tab; // 'profile' from query string
const hash = this.args.hash; // Fragment after #
}
}
Layout System
Layout Definition
class MainLayout extends Jqhtml_Layout {
async on_render() {
this.$.html(`
<header>Navigation</header>
<main $sid="content"></main> <!-- REQUIRED: Routes render here -->
<footer>Footer</footer>
`);
}
async on_route_change(old_route, new_route) {
// Update active navigation state
this.$find('.nav-link').removeClass('active');
this.$find(`[href="${new_route.path}"]`).addClass('active');
}
should_rerender() {
return false; // Layouts persist across route changes
}
async pre_dispatch(route_info) {
// Can intercept routing at layout level
if (route_info.meta.requiresAuth && !this.data.user) {
return '/login'; // Redirect
}
return true;
}
}
Layout Persistence
- Layouts never re-render during route changes within same layout
- Layout instance persists until different layout needed
$sid="content"element required for route injection point
SPA Application Container
Basic SPA Setup
class App extends Jqhtml_SPA {
async on_create() {
// Register all routes
HomeRoute.init();
UserRoute.init();
AdminRoute.init();
// Register layouts
Jqhtml_Router.register_layout('MainLayout', MainLayout);
Jqhtml_Router.register_layout('AdminLayout', AdminLayout);
// Set default layout
Jqhtml_Router.set_default_layout('MainLayout');
// Initialize router
await this.init_router({
default_layout: 'MainLayout'
});
}
async pre_dispatch(route_info) {
// Global route guard
// Runs before layout.pre_dispatch()
return true;
}
async post_dispatch(route_info) {
// Global post-navigation logic
// Runs after layout.post_dispatch()
}
}
// Mount application
$('#app').append(new App().$);
Navigation Methods
Programmatic Navigation
// Navigate to URL
Jqhtml_Router.dispatch('/users/123');
// Navigate with replace (no history entry)
Jqhtml_Router.replace('/login');
// Navigate with parameters
const url = Jqhtml_Router.url('user-profile', { id: 123, tab: 'settings' });
Jqhtml_Router.dispatch(url);
// From within route component
this.dispatch({ id: 456 }); // Merges with current args
Link Interception
- All
<a>tags automatically intercepted for same-origin navigation - External links and
target="_blank"not intercepted - Ctrl/Cmd+Click opens in new tab (not intercepted)
URL Building
// Build URL from route name
Jqhtml_Router.url('user-profile', { id: 123 }); // '/users/123'
// Build URL from pattern
Jqhtml_Router.build_url('/users/:id', { id: 123, tab: 'info' }); // '/users/123?tab=info'
// From route class
UserRoute.url({ id: 123 }); // '/users/123'
Dispatch Lifecycle
Execution Order
- URL Matching - Parse URL, extract parameters
- App.pre_dispatch() - Global guard, can cancel/redirect
- Layout Resolution - Create/reuse layout based on route
- Layout.pre_dispatch() - Layout-level guard
- Route Instantiation - Create route component with args
- Route.pre_dispatch() - Route-level guard
- Layout._render_route() - Inject route into layout
- Route Lifecycle - Standard component lifecycle
- Layout.on_route_change() - Notify layout of change
- Route.post_dispatch() - Route complete
- Layout.post_dispatch() - Layout handling complete
- App.post_dispatch() - Global post-navigation
Guard Return Values
async pre_dispatch(route_info) {
return true; // Continue navigation
return false; // Cancel navigation
return '/login'; // Redirect to different route
}
Router State
Access Current State
// Static access
Jqhtml_Router.state = {
route: 'UserRoute', // Current route component name
layout: 'MainLayout', // Current layout name
url: '/users/123', // Current URL path
args: { id: '123' }, // Current parameters
hash: 'section' // Current hash fragment
};
// Get current route info
const current = Jqhtml_Router.current_route_info;
// { url, path, args, hash, meta, component_name, component_class, layout }
// From SPA class
Jqhtml_SPA.state; // Same as Jqhtml_Router.state
Jqhtml_SPA.current_route; // Current route info
Hash Routing (file:// Protocol)
Automatic Detection
// Router automatically uses hash routing for file:// URLs
// file:///app/index.html#/users/123
// Translates to route: /users/123
// Hash routing format
window.location.href = 'file:///path/index.html#/users/123';
// Router sees: /users/123
// Regular routing format (http/https)
window.location.href = 'https://example.com/users/123';
// Router sees: /users/123
Manual Hash Control
if (Jqhtml_Router.use_hash_routing) {
// In hash mode
window.location.hash = '#/users/123';
} else {
// In regular mode
window.history.pushState({}, '', '/users/123');
}
Route Metadata & Named Routes
Named Route Pattern
class UserProfileRoute extends Jqhtml_Route {
static routes = ['/users/:id/profile'];
static meta = {
name: 'user-profile', // Reference name
requiresAuth: true,
title: 'User Profile'
};
}
// Navigate by name
Jqhtml_Router.url('user-profile', { id: 123 });
UserProfileRoute.dispatch({ id: 123 });
Advanced Patterns
Nested Route Components
class DashboardRoute extends Jqhtml_Route {
async on_render() {
this.$.html(`
<div class="dashboard">
<Sidebar />
<div $sid="dashboard-content">
<!-- Sub-routes could render here -->
</div>
</div>
`);
}
}
Route-Level Data Loading
class UserRoute extends Jqhtml_Route {
async on_load() {
// Runs during component load phase
// Data fetching happens in parallel with siblings
const userId = this.args.id;
this.data.user = await fetch(`/api/users/${userId}`).then(r => r.json());
}
}
Authentication Flow
class AuthLayout extends Jqhtml_Layout {
async pre_dispatch(route_info) {
const token = localStorage.getItem('auth_token');
if (!token && route_info.meta.requiresAuth) {
// Store intended destination
sessionStorage.setItem('redirect_after_login', route_info.url);
return '/login'; // Redirect to login
}
return true;
}
}
class LoginRoute extends Jqhtml_Route {
async handleLogin() {
const token = await authenticate(this.data.credentials);
localStorage.setItem('auth_token', token);
// Redirect to intended destination or home
const redirect = sessionStorage.getItem('redirect_after_login') || '/';
sessionStorage.removeItem('redirect_after_login');
Jqhtml_Router.dispatch(redirect);
}
}
Query String Handling
// URL: /search?q=jqhtml&category=docs&page=2
class SearchRoute extends Jqhtml_Route {
static routes = ['/search'];
async on_load() {
const query = this.args.q; // 'jqhtml'
const category = this.args.category; // 'docs'
const page = parseInt(this.args.page) || 1; // 2
}
nextPage() {
// Navigate with updated query params
this.dispatch({
page: (parseInt(this.args.page) || 1) + 1
});
}
}
Critical Invariants
- Layouts must have element with
$sid="content"for route injection - Route dispatch is asynchronous and can be cancelled at multiple points
- Layout instances persist across same-layout route changes
- Routes are destroyed and recreated on each navigation
- Hash routing auto-enabled for file:// protocol, cannot be disabled
- All same-origin link clicks are intercepted unless explicitly prevented
- Router state is global singleton, only one router instance exists
- Route parameters are always strings, cast as needed
- Query parameters merge with route parameters in args object
- Guard functions execute in strict order: app → layout → route