# JQHTML SPA Router System - LLM Reference
## Core Architecture
- Static router class `Jqhtml_Router` manages 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 for `file://` protocol
- Standard routing for `http://` and `https://` protocols
## Route Definition
### Basic Route Class
```javascript
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
```javascript
static routes = [
'/', // Exact match
'/users', // Static path
'/users/:id', // Parameter capture
'/posts/:id/comments', // Multiple segments
'/admin/:section/:id' // Multiple parameters
];
```
### Parameter Access
```javascript
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
```javascript
class MainLayout extends Jqhtml_Layout {
async on_render() {
this.$.html(`
`);
}
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
```javascript
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
```javascript
// 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 `` 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
```javascript
// 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
1. **URL Matching** - Parse URL, extract parameters
2. **App.pre_dispatch()** - Global guard, can cancel/redirect
3. **Layout Resolution** - Create/reuse layout based on route
4. **Layout.pre_dispatch()** - Layout-level guard
5. **Route Instantiation** - Create route component with args
6. **Route.pre_dispatch()** - Route-level guard
7. **Layout._render_route()** - Inject route into layout
8. **Route Lifecycle** - Standard component lifecycle
9. **Layout.on_route_change()** - Notify layout of change
10. **Route.post_dispatch()** - Route complete
11. **Layout.post_dispatch()** - Layout handling complete
12. **App.post_dispatch()** - Global post-navigation
### Guard Return Values
```javascript
async pre_dispatch(route_info) {
return true; // Continue navigation
return false; // Cancel navigation
return '/login'; // Redirect to different route
}
```
## Router State
### Access Current State
```javascript
// 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
```javascript
// 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
```javascript
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
```javascript
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
```javascript
class DashboardRoute extends Jqhtml_Route {
async on_render() {
this.$.html(`
`);
}
}
```
### Route-Level Data Loading
```javascript
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
```javascript
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
```javascript
// 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
1. Layouts must have element with `$sid="content"` for route injection
2. Route dispatch is asynchronous and can be cancelled at multiple points
3. Layout instances persist across same-layout route changes
4. Routes are destroyed and recreated on each navigation
5. Hash routing auto-enabled for file:// protocol, cannot be disabled
6. All same-origin link clicks are intercepted unless explicitly prevented
7. Router state is global singleton, only one router instance exists
8. Route parameters are always strings, cast as needed
9. Query parameters merge with route parameters in args object
10. Guard functions execute in strict order: app → layout → route