From 4798f4b3c6a46fd7e93b8d48a6ea4eb788ab2866 Mon Sep 17 00:00:00 2001 From: Kevand Date: Sat, 4 Apr 2026 19:46:29 +0100 Subject: [PATCH] added dismount lifecycle --- src/lifecycle.ts | 36 ++++++++++++++++++++++++++++++++++ src/router.ts | 51 ++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 src/lifecycle.ts diff --git a/src/lifecycle.ts b/src/lifecycle.ts new file mode 100644 index 0000000..8e36603 --- /dev/null +++ b/src/lifecycle.ts @@ -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) + } +} \ No newline at end of file diff --git a/src/router.ts b/src/router.ts index 52bd20b..89a2093 100644 --- a/src/router.ts +++ b/src/router.ts @@ -6,9 +6,11 @@ export interface RouteContext { query: QueryParams } -type NextFunction = () => void; -type RouteHandler = (ctx: RouteContext) => void; -type RouteGuard = (ctx: RouteContext, next: NextFunction) => void; +export type NextFunction = () => void; +export type RouteHandler = (ctx: RouteContext) => HTMLElement; +export type RouteGuard = (ctx: RouteContext, next: NextFunction) => void; + +export type LayoutHandler = () => { view: HTMLElement, slot: HTMLElement } interface Route { path: string; @@ -16,6 +18,7 @@ interface Route { keys: string[], handler: RouteHandler; guards: RouteGuard[]; + layout?: LayoutHandler } export class Router { @@ -24,6 +27,9 @@ export class Router { private rootElement: HTMLElement | null = null; private isTransitioning = false; + private currentLayout: LayoutHandler | null = null; + private currentSlot: HTMLElement | null = null; + private static instance: Router | null = null; 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 regexpPath = path.replace(/:(\w+)/g, (_, key) => { keys.push(key); @@ -51,7 +57,7 @@ export class Router { this.routes.push({ path, regex: new RegExp(`^${regexpPath}$`), - keys, handler, guards + keys, handler, guards, layout }) } @@ -76,9 +82,33 @@ export class Router { const ctx: RouteContext = { params, query }; 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(() => { - 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; } @@ -100,14 +130,15 @@ export class Router { next(); } - private performTransition(updateDomFn: () => void) { - if (!this.rootElement) { + private performTransition(updateDomFn: () => void, targetElement: HTMLElement | null) { + const el = targetElement || this.rootElement; + + if (!el) { updateDomFn(); return; } this.isTransitioning = true; - this.rootElement.classList.add('fading'); setTimeout(() => {