UNPKG

vasille-router

Version:

The first Developer eXperience Orientated front-end framework (router library).

129 lines (128 loc) 4.61 kB
import { App, Fragment, Reference, reportError } from "vasille"; import { Runner } from "vasille/web-runner"; import { composeUrl, Router as AbstractRouter } from "../router.js"; function build(node, run, name) { const child = new Fragment({}, node.runner, name); node.create(child, run); } export class Router extends AbstractRouter { constructor(window, location, node, init) { super(init); this.currentUrl = new Reference(""); this.loadingUrl = new Reference(null); this.webInit = init; this.window = window; this.location = location; this.node = node; this.currentUrl.$ = location.pathname; build(node, node => (this.loadingNode = node), ":router:loading-screen"); build(node, node => (this.contentNode = node), ":router:content-screen"); build(node, node => (this.overlayNode = node), ":router:loading-overlay"); window.addEventListener("popstate", () => { this.doNavigate(location.href, false, "loading-screen"); }); this.doNavigate(location.href, true, "loading-screen"); } navigate(route, params, mode) { super.navigate(route, params, mode); } /** * Do a silent navigation without any modification in DOM until successful * @param route target route * @param params target route params * @throws {Error} a lot of errors */ silentNavigate(route, params) { return this.prepareNavigation(composeUrl(route, params), true, true, "silent"); } reload() { this.doNavigate(this.currentUrl.$, true, "loading-screen"); } doNavigate(url, canNavigate, mode) { this.prepareNavigation(url, canNavigate, false, mode).catch(e => { this.clearLoadings(); reportError(e); }); } parseUrl(url) { const parsed = new URL(url, this.location.origin); return [ parsed.pathname, [...parsed.searchParams.keys()].reduce((prev, key) => { const value = parsed.searchParams.getAll(key); return { ...prev, [key]: value.length === 1 ? value[0] : value, }; }, {}), parsed.hash, ]; } async loadTarget(target, props, mode) { this.loadingUrl.$ = props.path; try { const { loadingScreen, loadingOverlay } = this.webInit; if (mode === "loading-screen" && loadingScreen) { this.clearNode(this.contentNode); build(this.loadingNode, node => loadingScreen({}, node), "::"); } if (mode === "loading-overlay" && loadingOverlay) { build(this.overlayNode, node => loadingOverlay({}, node), "::"); } await this.renderScreen(target.screen, props, "found"); if (this.location.href !== props.url) { this.window.history.pushState({}, "", props.url); } this.currentUrl.$ = props.url; } catch (error) { if (mode !== "silent") { this.clearNode(this.contentNode); } throw error; } finally { this.clearLoadings(); this.loadingUrl.$ = null; } } async renderScreen(screen, props, scope) { const children = this.contentNode.children; const oldChildren = [...children]; let ctx = null; build(this.contentNode, node => (ctx = node), "::"); /* istanbul ignore else */ if (ctx) { await screen(props, ctx); } oldChildren.forEach(node => { node.destroy(); children.delete(node); }); if (scope === "not-found") { this.window.history.replaceState({}, "", "/"); } } clearLoadings() { this.clearNode(this.loadingNode); this.clearNode(this.overlayNode); } clearNode(node) { const { children } = node; children.forEach(node => node.destroy()); children.clear(); } } export function routeApp(node, window, location, init, debugUi) { const runner = new Runner(debugUi ?? false, window.document); const app = new App(node, runner, {}); app.create(new Fragment({}, runner, ":router:root"), node => { const router = new Router(window, location, node, init); Object.defineProperty(runner, "router", { get() { return router; }, }); }); return app; }