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>
355 lines
9.5 KiB
Markdown
Executable File
355 lines
9.5 KiB
Markdown
Executable File
# 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(`
|
|
<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
|
|
```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 `<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
|
|
```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(`
|
|
<div class="dashboard">
|
|
<Sidebar />
|
|
<div $sid="dashboard-content">
|
|
<!-- Sub-routes could render here -->
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 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 |