init
This commit is contained in:
123
src/router.ts
Normal file
123
src/router.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
type RouteParams = Record<string, string>;
|
||||
type QueryParams = Record<string, string>;
|
||||
|
||||
export interface RouteContext {
|
||||
params: RouteParams;
|
||||
query: QueryParams
|
||||
}
|
||||
|
||||
type NextFunction = () => void;
|
||||
type RouteHandler = (ctx: RouteContext) => void;
|
||||
type RouteGuard = (ctx: RouteContext, next: NextFunction) => void;
|
||||
|
||||
interface Route {
|
||||
path: string;
|
||||
regex: RegExp,
|
||||
keys: string[],
|
||||
handler: RouteHandler;
|
||||
guards: RouteGuard[];
|
||||
}
|
||||
|
||||
export class Router {
|
||||
|
||||
private routes: Route[] = [];
|
||||
private rootElement: HTMLElement | null = null;
|
||||
private isTransitioning = false;
|
||||
|
||||
private static instance: Router | null = null;
|
||||
|
||||
constructor(rootId: string) {
|
||||
this.rootElement = document.getElementById(rootId);
|
||||
|
||||
Router.instance = this;
|
||||
|
||||
window.addEventListener('hashchange', () => this.handleRoute());
|
||||
window.addEventListener('load', () => this.handleRoute())
|
||||
}
|
||||
|
||||
public static reload() {
|
||||
if (Router.instance) {
|
||||
Router.instance.handleRoute();
|
||||
}
|
||||
}
|
||||
|
||||
public addRoute(path: string, handler: RouteHandler, guards: RouteGuard[] = []) {
|
||||
const keys: string[] = [];
|
||||
const regexpPath = path.replace(/:(\w+)/g, (_, key) => {
|
||||
keys.push(key);
|
||||
return '([^/]+)'
|
||||
})
|
||||
|
||||
this.routes.push({
|
||||
path,
|
||||
regex: new RegExp(`^${regexpPath}$`),
|
||||
keys, handler, guards
|
||||
})
|
||||
}
|
||||
|
||||
private handleRoute() {
|
||||
if (this.isTransitioning) return;
|
||||
|
||||
const hash = window.location.hash.slice(1) || '/';
|
||||
const [path, queryString] = hash.split("?");
|
||||
|
||||
const query: QueryParams = {};
|
||||
new URLSearchParams(queryString).forEach((val, key) => {
|
||||
query[key] = val
|
||||
});
|
||||
|
||||
for (const route of this.routes) {
|
||||
const match = path.match(route.regex);
|
||||
|
||||
if (match) {
|
||||
const params: RouteParams = {};
|
||||
route.keys.forEach((key, index) => params[key] = match[index + 1]);
|
||||
|
||||
const ctx: RouteContext = { params, query };
|
||||
|
||||
this.runGuards(route.guards, ctx, () => {
|
||||
this.performTransition(() => {
|
||||
route.handler(ctx);
|
||||
})
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private runGuards(guards: RouteGuard[], ctx: RouteContext, callback: () => void) {
|
||||
let index = 0;
|
||||
|
||||
const next = () => {
|
||||
if (index < guards.length) {
|
||||
guards[index++](ctx, next)
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
private performTransition(updateDomFn: () => void) {
|
||||
if (!this.rootElement) {
|
||||
updateDomFn();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isTransitioning = true;
|
||||
|
||||
this.rootElement.classList.add('fading');
|
||||
|
||||
setTimeout(() => {
|
||||
updateDomFn();
|
||||
this.rootElement?.classList.remove('fading');
|
||||
this.isTransitioning = false;
|
||||
}, 100)
|
||||
}
|
||||
|
||||
static navigate(path: string) {
|
||||
window.location.hash = path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user