@wavelengthusaf/client-router
v0.0.4
Published
Native TypeScript Router
Keywords
Readme
Wavelength Router
A lightweight, native TypeScript web components router for modern web applications.
Table of Contents
Getting Started
Prerequisites
- Node.js 18+ and npm
- A modern browser with native web components support
Installation
Clone the repository and install dependencies:
git clone https://github.com/your-org/wavelength-router.git
cd wavelength-router
npm installHusky Setup
This project uses Husky to run linting and tests before each commit. If you're setting up the project for the first time or if Husky hooks aren't working, initialize it with:
npm run prepareOr manually initialize:
npx husky initThis ensures that before every commit, Husky will:
- Run unit tests via
npm run test - Run ESLint and Prettier via
npm run lint
If either fails, the commit will be blocked.
Adding to Your Project
Option 1: npm Package (Published)
Once published to npm, install the package:
npm install @wavelengthusaf/client-routerThen import the elements in your entry point:
// main.ts or main.js
import '@wavelengthusaf/client-router';Option 2: Local Development
For local development or testing changes:
npm run buildThis compiles TypeScript to dist/. Then in your consuming project:
import '@wavelengthusaf/client-router';Usage
You can implement routing using either the HTML template approach or pure TypeScript.
HTML Template Approach
Use the custom elements directly in your HTML. This is ideal for simpler applications or when you want declarative routing.
Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My App</title>
</head>
<body>
<!-- Navigation -->
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/users/123">User Profile</a>
</nav>
<!-- Router Container - renders matched route component here -->
<wavelength-router>
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route path="/about" component="about-page"></wavelength-route>
<wavelength-route
path="/users/:userId"
component="user-page"
></wavelength-route>
<wavelength-route path="*" component="not-found-page"></wavelength-route>
</wavelength-router>
<script type="module">
import '@wavelengthusaf/client-router';
// Define your page components as custom elements
customElements.define(
'home-page',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>Welcome Home</h1>';
}
},
);
customElements.define(
'about-page',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>About Us</h1><p>We are awesome.</p>';
}
},
);
customElements.define(
'user-page',
class extends HTMLElement {
connectedCallback() {
// Access route parameters via routeContext
const userId = this.routeContext?.routeParams?.userId;
this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
}
},
);
customElements.define(
'not-found-page',
class extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>404 - Page Not Found</h1>';
}
},
);
</script>
</body>
</html>How It Works
<wavelength-router>acts as the router container and outlet- It scans for
<wavelength-route>children and extracts route definitions - When the URL changes, the router matches the path and mounts the corresponding component
- The component is rendered inside the
<wavelength-router>element
TypeScript Approach
For more control, you can use the Router class directly and define routes programmatically.
Basic Example
import { Router } from '@wavelengthusaf/client-router';
import type {
RouteContext,
RouteDefinition,
RouteGuard,
} from '@wavelengthusaf/client-router';
// Define your page components
class HomePage extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>Welcome Home</h1>';
}
}
class AboutPage extends HTMLElement {
connectedCallback() {
this.innerHTML = '<h1>About Us</h1>';
}
}
class UserPage extends HTMLElement {
connectedCallback() {
const userId = this.routeContext?.routeParams?.userId;
this.innerHTML = `<h1>User Profile</h1><p>User ID: ${userId}</p>`;
}
}
// Register components
customElements.define('home-page', HomePage);
customElements.define('about-page', AboutPage);
customElements.define('user-page', UserPage);
// Define routes using RouteDefinition objects
const routes: RouteDefinition[] = [
{
path: '/',
routeComponent: {
mount(container: HTMLElement, context: RouteContext) {
const element = document.createElement('home-page') as HTMLElement & {
routeContext?: RouteContext;
};
element.routeContext = context;
container.appendChild(element);
},
unmount() {
// Optional cleanup
},
},
},
{
path: '/about',
routeComponent: {
mount(container: HTMLElement, context: RouteContext) {
const element = document.createElement('about-page') as HTMLElement & {
routeContext?: RouteContext;
};
element.routeContext = context;
container.appendChild(element);
},
},
},
{
path: '/users/:userId',
routeComponent: {
mount(container: HTMLElement, context: RouteContext) {
const element = document.createElement('user-page') as HTMLElement & {
routeContext?: RouteContext;
};
element.routeContext = context;
container.appendChild(element);
},
},
},
];
// Create and start the router
const router = new Router(routes, {
outlet: document.getElementById('app')!, // Container element
hashMode: false, // Use hash routing (optional)
});
router.start();RouteDefinition Structure
Each route requires:
path: The URL path pattern (supports:paramsyntax for dynamic segments)routeComponent: An object with amountfunction (and optionalunmount)
interface RouteDefinition {
path: string; // e.g., '/users/:id' or '/products/:category/:id'
routeComponent: {
mount(container: HTMLElement, context: RouteContext): void;
unmount?(): void; // Optional cleanup function
};
}API Reference
<wavelength-router> Attributes
| Attribute | Type | Description |
| ----------- | ------ | ------------------------------------------------------- |
| hash-mode | flag | Enable hash-based routing (#/path) instead of history |
| guard | string | Name of a global guard function on window |
Example with Hash Mode
<wavelength-router hash-mode>
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route path="/profile" component="profile-page"></wavelength-route>
</wavelength-router>URL becomes: https://example.com/#/profile
<wavelength-route> Attributes
| Attribute | Type | Description |
| ----------- | ------ | ------------------------------------------------ |
| path | string | URL path pattern (supports :param syntax) |
| component | string | Custom element tag name to render for this route |
Path Patterns
- Static:
/about,/dashboard - Dynamic:
/users/:userId,/posts/:category/:postId - Wildcard:
*(catches all unmatched routes)
RouteContext
Passed to your component's routeContext property and the mount function:
interface RouteContext {
path: string; // The matched path, e.g., '/users/123'
routeParams: {
// Extracted URL parameters
[key: string]: string;
};
query: URLSearchParams; // Query string parameters
}Accessing RouteContext in Components
class MyPage extends HTMLElement {
static get routeContext(): RouteContext | undefined {
return (this as unknown as { routeContext?: RouteContext }).routeContext;
}
connectedCallback() {
const ctx = MyPage.routeContext;
if (ctx) {
console.log('Current path:', ctx.path);
console.log('User ID:', ctx.routeParams.userId);
console.log('Search query:', ctx.query.get('search'));
}
}
}RouteGuard
Navigation guards allow you to intercept and redirect navigation. Define a guard function and reference it via the guard attribute.
// Define guard on window (required for HTML approach)
type NextCallback = (redirect?: string) => void;
(window as unknown as Record<string, unknown>).authGuard = (
to: RouteContext,
next: NextCallback,
) => {
const isAuthenticated = checkAuth(); // Your auth logic
if (to.path.startsWith('/protected') && !isAuthenticated) {
next('/login'); // Redirect to login
} else {
next(); // Allow navigation
}
};<wavelength-router guard="authGuard">
<wavelength-route path="/" component="home-page"></wavelength-route>
<wavelength-route
path="/protected"
component="protected-page"
></wavelength-route>
<wavelength-route path="/login" component="login-page"></wavelength-route>
</wavelength-router>Guard Flow:
- Guard receives
RouteContextwith destination path - Guard calls
next()to allow navigation ornext('/redirect-path')to redirect
Programmatic Navigation
The router provides navigation methods:
const routerElement = document.querySelector('wavelength-router');
// Navigate to a new path (adds to history)
routerElement.navigate('/about');
// Navigate and replace current entry (no history)
routerElement.replace('/new-page');
// Go back in history
routerElement.back();Best Practices
1. Always Define a Wildcard Route
Provide a catch-all route for 404 handling:
<wavelength-route path="*" component="not-found-page"></wavelength-route>2. Access RouteContext Correctly
The routeContext is injected into your custom element. Always check for its existence:
class UserPage extends HTMLElement {
connectedCallback() {
const ctx = (this as unknown as { routeContext?: RouteContext })
.routeContext;
if (ctx) {
this.innerHTML = `<p>User: ${ctx.routeParams.userId}</p>`;
}
}
}3. Use Hash Mode for Static Hosts
If deploying to static hosting without server configuration, use hash mode:
<wavelength-router hash-mode> ... </wavelength-router>This avoids the need for server-side routing rules.
4. Clean Up in Route Components
Implement cleanup logic in the unmount function if needed:
const routes: RouteDefinition[] = [
{
path: '/chat',
routeComponent: {
mount(container, context) {
const chat = document.createElement('chat-widget');
container.appendChild(chat);
},
unmount() {
// Close WebSocket connections, remove event listeners, etc.
},
},
},
];5. Route Guards for Authentication
Always protect authenticated routes:
(window as unknown as Record<string, unknown>).requireAuth = (ctx, next) => {
if (isProtectedPath(ctx.path) && !getCurrentUser()) {
next('/login');
} else {
next();
}
};6. URL Encoding
Route parameters are automatically URL-decoded. If you pass special characters, they will be properly handled.
7. Order Routes by Specificity
More specific routes should be defined before general ones:
<!-- Correct order -->
<wavelength-route path="/users/:id" component="user-page"></wavelength-route>
<wavelength-route path="/users" component="users-list"></wavelength-route>
<wavelength-route path="*" component="not-found"></wavelength-route>8. Avoid Memory Leaks
Always clean up when routes unmount:
routeComponent: {
let interval: number;
mount(container) {
const page = document.createElement('my-page');
container.appendChild(page);
// Store reference for cleanup
interval = setInterval(() => updateTime(), 1000);
},
unmount() {
clearInterval(interval);
},
}Development
# Run tests
npm run test
# Run tests with coverage
npm run test -- --coverage
# Lint and format
npm run lint
npm run prettier
# Build for production
npm run build
# Run Storybook for component development
npm run storybookLicense
ISC - Wavelength
