68 lines
1.6 KiB
TypeScript
68 lines
1.6 KiB
TypeScript
type PageHandler = (
|
|
outlet: HTMLElement,
|
|
params: Record<string, string>,
|
|
) => void | Promise<void>;
|
|
|
|
interface Route {
|
|
pattern: RegExp;
|
|
keys: string[];
|
|
handler: PageHandler;
|
|
}
|
|
|
|
const routes: Route[] = [];
|
|
let notFoundHandler: PageHandler | null = null;
|
|
|
|
export function route(path: string, handler: PageHandler) {
|
|
if (path === "*") {
|
|
notFoundHandler = handler;
|
|
return;
|
|
}
|
|
const keys: string[] = [];
|
|
const pattern = path.replace(/:(\w+)/g, (_, key: string) => {
|
|
keys.push(key);
|
|
return "([^/]+)";
|
|
});
|
|
routes.push({ pattern: new RegExp(`^${pattern}$`), keys, handler });
|
|
}
|
|
|
|
export function navigate(path: string) {
|
|
history.pushState(null, "", path);
|
|
void render();
|
|
}
|
|
|
|
async function render() {
|
|
const path = location.pathname;
|
|
const outlet = document.getElementById("outlet")!;
|
|
|
|
for (const r of routes) {
|
|
const match = path.match(r.pattern);
|
|
if (match) {
|
|
const params: Record<string, string> = {};
|
|
r.keys.forEach((key, i) => {
|
|
params[key] = match[i + 1]!;
|
|
});
|
|
outlet.innerHTML = "";
|
|
await r.handler(outlet, params);
|
|
return;
|
|
}
|
|
}
|
|
|
|
outlet.innerHTML = "";
|
|
if (notFoundHandler) {
|
|
await notFoundHandler(outlet, {});
|
|
}
|
|
}
|
|
|
|
export function initRouter() {
|
|
window.addEventListener("popstate", () => void render());
|
|
|
|
document.addEventListener("click", (e) => {
|
|
const anchor = (e.target as HTMLElement).closest("a");
|
|
if (anchor?.origin === location.origin && !anchor.hasAttribute("download")) {
|
|
e.preventDefault();
|
|
navigate(anchor.pathname);
|
|
}
|
|
});
|
|
|
|
void render();
|
|
}
|