Update Readme.md
This commit is contained in:
425
Readme.md
425
Readme.md
@@ -830,417 +830,138 @@ RegExp gives you **full control** over route matching with named capture groups.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 🎯 Route Parameters
|
## 🧭 `$.router(routes)` - Simple Router with Parameters
|
||||||
|
|
||||||
Parameters are automatically extracted and passed to your component:
|
Creates a hash-based router with support for `:param` parameters. Automatically cleans up pages when navigating away.
|
||||||
|
|
||||||
|
### 📋 Route Parameters (Human-Friendly)
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Route: '/users/:id'
|
const routes = [
|
||||||
// URL: '#/users/42'
|
{ path: '/', component: HomePage },
|
||||||
{ path: '/users/:id', component: (params) => {
|
{ path: '/about', component: AboutPage },
|
||||||
console.log(params.id); // "42"
|
{ path: '/user/:id', component: UserPage }, // /user/42 → { id: '42' }
|
||||||
return UserPage(params);
|
{ path: '/user/:id/posts', component: UserPostsPage }, // /user/42/posts → { id: '42' }
|
||||||
}}
|
{ path: '/user/:id/posts/:pid', component: PostPage }, // /user/42/posts/123 → { id: '42', pid: '123' }
|
||||||
|
{ path: '/search/:query/page/:num', component: SearchPage }, // /search/js/page/2 → { query: 'js', num: '2' }
|
||||||
// Route: /^\/posts\/(?<id>\d+)$/
|
];
|
||||||
// URL: '#/posts/123'
|
|
||||||
{ path: /^\/posts\/(?<id>\d+)$/, component: (params) => {
|
|
||||||
console.log(params.id); // "123"
|
|
||||||
return PostPage(params);
|
|
||||||
}}
|
|
||||||
|
|
||||||
// Multiple parameters
|
|
||||||
// Route: '/products/:category/:id'
|
|
||||||
// URL: '#/products/electronics/42'
|
|
||||||
{ path: '/products/:category/:id', component: (params) => {
|
|
||||||
console.log(params.category); // "electronics"
|
|
||||||
console.log(params.id); // "42"
|
|
||||||
return ProductPage(params);
|
|
||||||
}}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 🎯 Accessing Parameters in Pages
|
||||||
|
|
||||||
### 🔄 Automatic Page Cleanup
|
Parameters are automatically extracted and passed to your page component:
|
||||||
|
|
||||||
The router **automatically cleans up** pages when navigating away:
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// pages/UserPage.js
|
// pages/UserPage.js
|
||||||
import { $, html } from 'sigpro';
|
import { $, html } from 'sigpro';
|
||||||
|
|
||||||
export default (params) => $.page(({ onUnmount }) => {
|
export default (params) => $.page(() => {
|
||||||
|
// /user/42 → params = { id: '42' }
|
||||||
|
const userId = params.id;
|
||||||
const userData = $(null);
|
const userData = $(null);
|
||||||
const loading = $(true);
|
|
||||||
|
|
||||||
// Set up polling
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchUserData(params.id);
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
// Register cleanup
|
|
||||||
onUnmount(() => {
|
|
||||||
clearInterval(interval); // ✅ Auto-cleaned on navigation
|
|
||||||
console.log('UserPage cleaned up');
|
|
||||||
});
|
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div>
|
<div>
|
||||||
${loading() ? 'Loading...' : html`<h1>${userData().name}</h1>`}
|
<h1>User Profile: ${userId}</h1>
|
||||||
|
<p>Loading user data...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// pages/PostPage.js
|
||||||
|
export default (params) => $.page(() => {
|
||||||
|
// /user/42/posts/123 → params = { id: '42', pid: '123' }
|
||||||
|
const { id, pid } = params;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div>
|
||||||
|
<h1>Post ${pid} from user ${id}</h1>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
All `$.effect`, `setInterval`, event listeners, and subscriptions are automatically cleaned up.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🧭 Navigation
|
### 🧭 Navigation
|
||||||
|
|
||||||
#### Programmatic Navigation
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { $ } from 'sigpro';
|
// Programmatic navigation
|
||||||
|
$.router.go('/user/42');
|
||||||
|
$.router.go('/search/javascript/page/2');
|
||||||
|
$.router.go('about'); // Same as '/about' (auto-adds leading slash)
|
||||||
|
|
||||||
// Go to specific route
|
// Link navigation (in templates)
|
||||||
$.router.go('/about');
|
html`
|
||||||
$.router.go('/users/42');
|
|
||||||
$.router.go('/products/electronics/123');
|
|
||||||
|
|
||||||
// Automatically adds leading slash if missing
|
|
||||||
$.router.go('about'); // Same as '/about'
|
|
||||||
$.router.go('users/42'); // Same as '/users/42'
|
|
||||||
|
|
||||||
// Works with hash
|
|
||||||
$.router.go('#/about'); // Also works
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Link Navigation
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// In your HTML templates
|
|
||||||
const navigation = html`
|
|
||||||
<nav>
|
<nav>
|
||||||
<a href="#/">Home</a>
|
<a href="#/">Home</a>
|
||||||
<a href="#/about">About</a>
|
<a href="#/user/42">Profile</a>
|
||||||
<a href="#/users/42">User 42</a>
|
<button @click=${() => $.router.go('/contact')}>Contact</button>
|
||||||
|
|
||||||
<!-- Or use @click for programmatic -->
|
|
||||||
<button @click=${() => $.router.go('/contact')}>
|
|
||||||
Contact
|
|
||||||
</button>
|
|
||||||
</nav>
|
</nav>
|
||||||
`;
|
`;
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Current Route Info
|
### 🔄 Automatic Page Cleanup
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// Get current path (without hash)
|
export default (params) => $.page(({ onUnmount }) => {
|
||||||
const currentPath = window.location.hash.replace(/^#/, '') || '/';
|
// Set up interval
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
fetchData(params.id);
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
// Listen to route changes
|
// Auto-cleaned when navigating away
|
||||||
window.addEventListener('hashchange', () => {
|
onUnmount(() => clearInterval(interval));
|
||||||
console.log('Route changed to:', window.location.hash);
|
|
||||||
|
return html`<div>Page content</div>`;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
### 📦 Usage in Templates
|
||||||
|
|
||||||
### 📑 Complete Examples
|
|
||||||
|
|
||||||
#### Basic SPA with Layout
|
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// app.js
|
|
||||||
import { $, html } from 'sigpro';
|
import { $, html } from 'sigpro';
|
||||||
import HomePage from './pages/Home.js';
|
import HomePage from './pages/Home.js';
|
||||||
import AboutPage from './pages/About.js';
|
import UserPage from './pages/User.js';
|
||||||
import ContactPage from './pages/Contact.js';
|
|
||||||
|
|
||||||
// Layout component with navigation
|
const routes = [
|
||||||
const AppLayout = () => html`
|
{ path: '/', component: HomePage },
|
||||||
|
{ path: '/user/:id', component: UserPage },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Mount router directly in your template
|
||||||
|
const App = () => html`
|
||||||
<div class="app">
|
<div class="app">
|
||||||
<header>
|
<header>My App</header>
|
||||||
<nav>
|
|
||||||
<a href="#/">Home</a>
|
|
||||||
<a href="#/about">About</a>
|
|
||||||
<a href="#/contact">Contact</a>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<!-- Router will inject pages here -->
|
${$.router(routes)} <!-- Router renders here -->
|
||||||
<div id="router-outlet"></div>
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
|
||||||
<p>© 2024 My App</p>
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Set up layout
|
document.body.appendChild(App());
|
||||||
document.body.appendChild(AppLayout());
|
|
||||||
|
|
||||||
// Create and mount router
|
|
||||||
const router = $.router([
|
|
||||||
{ path: '/', component: HomePage },
|
|
||||||
{ path: '/about', component: AboutPage },
|
|
||||||
{ path: '/contact', component: ContactPage },
|
|
||||||
]);
|
|
||||||
|
|
||||||
document.querySelector('#router-outlet').appendChild(router);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Blog with Dynamic Posts
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// pages/PostPage.js
|
|
||||||
import { $, html } from 'sigpro';
|
|
||||||
|
|
||||||
export default (params) => $.page(({ onUnmount }) => {
|
|
||||||
const post = $(null);
|
|
||||||
const loading = $(true);
|
|
||||||
const error = $(null);
|
|
||||||
|
|
||||||
// Fetch post data
|
|
||||||
const loadPost = async () => {
|
|
||||||
loading(true);
|
|
||||||
error(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`/api/posts/${params.id}`);
|
|
||||||
const data = await response.json();
|
|
||||||
post(data);
|
|
||||||
} catch (e) {
|
|
||||||
error('Failed to load post');
|
|
||||||
} finally {
|
|
||||||
loading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
loadPost();
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<article>
|
|
||||||
${loading() ? html`<div class="spinner">Loading...</div>` : ''}
|
|
||||||
${error() ? html`<div class="error">${error()}</div>` : ''}
|
|
||||||
${post() ? html`
|
|
||||||
<h1>${post().title}</h1>
|
|
||||||
<div class="content">${post().content}</div>
|
|
||||||
<a href="#/blog">← Back to blog</a>
|
|
||||||
` : ''}
|
|
||||||
</article>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
// routes
|
|
||||||
const routes = [
|
|
||||||
{ path: '/blog', component: BlogListPage },
|
|
||||||
{ path: '/blog/:id', component: BlogPostPage }, // Dynamic route
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
#### E-commerce with Categories
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// routes with multiple parameters
|
|
||||||
const routes = [
|
|
||||||
// Products by category
|
|
||||||
{ path: '/products/:category', component: (params) =>
|
|
||||||
ProductListPage({ category: params.category })
|
|
||||||
},
|
|
||||||
|
|
||||||
// Specific product
|
|
||||||
{ path: '/products/:category/:id', component: (params) =>
|
|
||||||
ProductDetailPage({
|
|
||||||
category: params.category,
|
|
||||||
id: params.id
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
// Search with query (using RegExp)
|
|
||||||
{
|
|
||||||
path: /^\/search\/(?<query>[^/]+)(?:\/page\/(?<page>\d+))?$/,
|
|
||||||
component: (params) => SearchPage({
|
|
||||||
query: decodeURIComponent(params.query),
|
|
||||||
page: params.page || 1
|
|
||||||
})
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ⚡ Advanced Features
|
|
||||||
|
|
||||||
#### Nested Routes
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Parent route
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/dashboard',
|
|
||||||
component: (params) => DashboardLayout(params, (nestedParams) => {
|
|
||||||
// Nested routing inside dashboard
|
|
||||||
const nestedRoutes = [
|
|
||||||
{ path: '/dashboard', component: DashboardHome },
|
|
||||||
{ path: '/dashboard/users', component: DashboardUsers },
|
|
||||||
{ path: '/dashboard/settings', component: DashboardSettings },
|
|
||||||
];
|
|
||||||
return $.router(nestedRoutes);
|
|
||||||
})
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Route Guards
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const routes = [
|
|
||||||
{
|
|
||||||
path: '/admin',
|
|
||||||
component: (params) => {
|
|
||||||
// Simple auth guard
|
|
||||||
if (!isAuthenticated()) {
|
|
||||||
$.router.go('/login');
|
|
||||||
return html`<!-- empty -->`;
|
|
||||||
}
|
|
||||||
return AdminPage(params);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 404 Handling
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const routes = [
|
|
||||||
{ path: '/', component: HomePage },
|
|
||||||
{ path: '/about', component: AboutPage },
|
|
||||||
// No match = automatic 404
|
|
||||||
// The router shows a default "404" if no route matches
|
|
||||||
];
|
|
||||||
|
|
||||||
// Or custom 404 page
|
|
||||||
const routesWith404 = [
|
|
||||||
{ path: '/', component: HomePage },
|
|
||||||
{ path: '/about', component: AboutPage },
|
|
||||||
// Add catch-all at the end
|
|
||||||
{ path: /.*/, component: () => Custom404Page() },
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🎯 API Reference
|
### 🎯 API Reference
|
||||||
|
|
||||||
#### `$.router(routes)`
|
#### `$.router(routes)`
|
||||||
|
- **routes**: `Array<{path: string, component: Function}>` - Route configurations with `:param` support
|
||||||
Creates a router instance.
|
- **Returns**: `HTMLDivElement` - Container that renders the current page
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|-----------|------|-------------|
|
|
||||||
| `routes` | `Array<Route>` | Array of route objects |
|
|
||||||
|
|
||||||
**Returns:** `HTMLDivElement` - Container that renders the current page
|
|
||||||
|
|
||||||
#### `$.router.go(path)`
|
#### `$.router.go(path)`
|
||||||
|
- **path**: `string` - Route path (automatically adds leading slash)
|
||||||
Navigates to a route programmatically.
|
|
||||||
|
|
||||||
| Parameter | Type | Description |
|
|
||||||
|-----------|------|-------------|
|
|
||||||
| `path` | `string` | Route path (automatically adds leading slash if missing) |
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```javascript
|
|
||||||
$.router.go('/users/42');
|
|
||||||
$.router.go('about'); // Same as '/about'
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Route Object
|
|
||||||
|
|
||||||
| Property | Type | Description |
|
|
||||||
|----------|------|-------------|
|
|
||||||
| `path` | `string` or `RegExp` | Route pattern to match |
|
|
||||||
| `component` | `Function` | Function that returns page content |
|
|
||||||
|
|
||||||
The `component` receives `params` object with extracted parameters.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 📊 Comparison
|
|
||||||
|
|
||||||
| Feature | SigPro Router | React Router | Vue Router |
|
|
||||||
|---------|--------------|--------------|------------|
|
|
||||||
| Bundle Size | **~0.5KB** | ~20KB | ~15KB |
|
|
||||||
| Dependencies | **0** | Many | Many |
|
|
||||||
| Hash-based | ✅ | ✅ | ✅ |
|
|
||||||
| History API | ❌ (by design) | ✅ | ✅ |
|
|
||||||
| Route params | ✅ | ✅ | ✅ |
|
|
||||||
| Nested routes | ✅ | ✅ | ✅ |
|
|
||||||
| Lazy loading | ✅ (native) | ✅ | ✅ |
|
|
||||||
| Auto cleanup | ✅ | ❌ | ❌ |
|
|
||||||
| No config | ✅ | ❌ | ❌ |
|
|
||||||
|
|
||||||
### 💡 Pro Tips
|
### 💡 Pro Tips
|
||||||
|
|
||||||
1. **Always define the most specific routes first**
|
1. **Order matters** - Define more specific routes first:
|
||||||
```javascript
|
```javascript
|
||||||
// ✅ Good
|
[
|
||||||
[
|
{ path: '/user/:id/edit', component: EditUser }, // More specific first
|
||||||
{ path: '/users/:id/edit', component: EditUser },
|
{ path: '/user/:id', component: ViewUser }, // Then generic
|
||||||
{ path: '/users/:id', component: ViewUser },
|
]
|
||||||
{ path: '/users', component: UserList },
|
```
|
||||||
]
|
|
||||||
|
|
||||||
// ❌ Bad (catch-all before specific)
|
2. **Cleanup is automatic** - All effects, intervals, and event listeners in `$.page` are cleaned up
|
||||||
[
|
|
||||||
{ path: '/users/:id', component: ViewUser }, // This matches '/users/edit' too!
|
|
||||||
{ path: '/users/:id/edit', component: EditUser }, // Never reached
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Use RegExp for validation**
|
3. **Zero config** - Just define routes and use them
|
||||||
```javascript
|
|
||||||
// Only numeric IDs
|
|
||||||
{ path: /^\/users\/(?<id>\d+)$/, component: UserPage }
|
|
||||||
|
|
||||||
// Only lowercase slugs
|
|
||||||
{ path: /^\/products\/(?<slug>[a-z-]+)$/, component: ProductPage }
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Lazy load pages for better performance**
|
|
||||||
```javascript
|
|
||||||
const routes = [
|
|
||||||
{ path: '/', component: () => import('./Home.js').then(m => m.default) },
|
|
||||||
{ path: '/about', component: () => import('./About.js').then(m => m.default) },
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Access route params anywhere**
|
|
||||||
```javascript
|
|
||||||
// In any component, not just pages
|
|
||||||
const currentParams = () => {
|
|
||||||
const match = window.location.hash.match(/^#\/users\/(?<id>\d+)$/);
|
|
||||||
return match?.groups || {};
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🎉 Why SigPro Router?
|
|
||||||
|
|
||||||
- **Zero config** - Works immediately
|
|
||||||
- **No dependencies** - Just vanilla JS
|
|
||||||
- **Automatic cleanup** - No memory leaks
|
|
||||||
- **Tiny footprint** - ~0.5KB minified
|
|
||||||
- **Hash-based** - Works everywhere
|
|
||||||
- **RegExp support** - Full routing power
|
|
||||||
- **Page components** - Built for `$.page`
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user