added dismount lifecycle

This commit is contained in:
Kevand
2026-04-04 19:46:29 +01:00
parent 6a73d483bb
commit 4798f4b3c6
2 changed files with 77 additions and 10 deletions

36
src/lifecycle.ts Normal file
View File

@@ -0,0 +1,36 @@
const CLEANUP_KEY = Symbol('cleanup_func');
export function onDismount(el: Node, fn: () => void) {
const element = el as any;
if (!element[CLEANUP_KEY]) {
element[CLEANUP_KEY] = [];
}
element[CLEANUP_KEY].push(fn);
}
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
mutation.removedNodes.forEach(node => {
deepCleanup(node);
})
})
})
observer.observe(document.body, {
childList: true,
subtree: true
});
function deepCleanup(node: Node) {
const el = node as any;
if (el[CLEANUP_KEY]) {
el[CLEANUP_KEY].forEach((fn: () => void) => fn());
el[CLEANUP_KEY] = null;
}
if (node.hasChildNodes()) {
node.childNodes.forEach(deepCleanup)
}
}

View File

@@ -6,9 +6,11 @@ export interface RouteContext {
query: QueryParams query: QueryParams
} }
type NextFunction = () => void; export type NextFunction = () => void;
type RouteHandler = (ctx: RouteContext) => void; export type RouteHandler = (ctx: RouteContext) => HTMLElement;
type RouteGuard = (ctx: RouteContext, next: NextFunction) => void; export type RouteGuard = (ctx: RouteContext, next: NextFunction) => void;
export type LayoutHandler = () => { view: HTMLElement, slot: HTMLElement }
interface Route { interface Route {
path: string; path: string;
@@ -16,6 +18,7 @@ interface Route {
keys: string[], keys: string[],
handler: RouteHandler; handler: RouteHandler;
guards: RouteGuard[]; guards: RouteGuard[];
layout?: LayoutHandler
} }
export class Router { export class Router {
@@ -24,6 +27,9 @@ export class Router {
private rootElement: HTMLElement | null = null; private rootElement: HTMLElement | null = null;
private isTransitioning = false; private isTransitioning = false;
private currentLayout: LayoutHandler | null = null;
private currentSlot: HTMLElement | null = null;
private static instance: Router | null = null; private static instance: Router | null = null;
constructor(rootId: string) { constructor(rootId: string) {
@@ -41,7 +47,7 @@ export class Router {
} }
} }
public addRoute(path: string, handler: RouteHandler, guards: RouteGuard[] = []) { public addRoute(path: string, handler: RouteHandler, guards: RouteGuard[] = [], layout?: LayoutHandler) {
const keys: string[] = []; const keys: string[] = [];
const regexpPath = path.replace(/:(\w+)/g, (_, key) => { const regexpPath = path.replace(/:(\w+)/g, (_, key) => {
keys.push(key); keys.push(key);
@@ -51,7 +57,7 @@ export class Router {
this.routes.push({ this.routes.push({
path, path,
regex: new RegExp(`^${regexpPath}$`), regex: new RegExp(`^${regexpPath}$`),
keys, handler, guards keys, handler, guards, layout
}) })
} }
@@ -76,9 +82,33 @@ export class Router {
const ctx: RouteContext = { params, query }; const ctx: RouteContext = { params, query };
this.runGuards(route.guards, ctx, () => { this.runGuards(route.guards, ctx, () => {
const pageElement = route.handler(ctx);
const layoutChanged = route.layout !== this.currentLayout;
const transitionTarget = layoutChanged ? this.rootElement : this.currentSlot
this.performTransition(() => { this.performTransition(() => {
route.handler(ctx); if (layoutChanged) {
}) if (route.layout) {
const { view, slot } = route.layout();
this.rootElement!.innerHTML = '';
this.rootElement!.appendChild(view);
this.currentSlot = slot;
this.currentLayout = route.layout;
} else {
this.rootElement!.innerHTML = '';
this.currentSlot = this.rootElement;
this.currentLayout = null;
}
}
if (this.currentSlot) {
this.currentSlot.innerHTML = ''
this.currentSlot.appendChild(pageElement);
}
}, transitionTarget)
}); });
return; return;
} }
@@ -100,14 +130,15 @@ export class Router {
next(); next();
} }
private performTransition(updateDomFn: () => void) { private performTransition(updateDomFn: () => void, targetElement: HTMLElement | null) {
if (!this.rootElement) { const el = targetElement || this.rootElement;
if (!el) {
updateDomFn(); updateDomFn();
return; return;
} }
this.isTransitioning = true; this.isTransitioning = true;
this.rootElement.classList.add('fading'); this.rootElement.classList.add('fading');
setTimeout(() => { setTimeout(() => {