UNPKG

vasille-router

Version:

The framework designed to build bulletproof frontends (router library).

172 lines (171 loc) 6.17 kB
import { App, Fragment, Reference, reportError } from "vasille"; import { Runner } from "vasille/web-runner"; import { Router as AbstractRouter } from "../router.js"; function build(node, run) { const child = new Fragment(node.runner); 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.V = location.pathname; build(node, node => (this.loadingNode = node)); build(node, node => (this.contentNode = node)); build(node, node => (this.overlayNode = node)); if (process.env.VASILLE_TARGET === "es5" && window.onpopstate !== null) { window.addEventListener("hashchange", () => { const path = location.hash.substring(1); if (path !== this.currentUrl.V) { this.doNavigate(path, false, "loading-screen"); } }); this.doNavigate(location.hash.substring(1) || location.href, true, "loading-screen"); } else { window.addEventListener("popstate", () => { this.doNavigate(location.href, false, "loading-screen"); }); this.doNavigate(location.href, true, "loading-screen"); } } /** * Navigate to new page, showing the loading screen */ goTo(url) { this.doNavigate(url, true, "loading-screen"); } /** * Navigate to new page in an AJAX way, showing a loading overlay */ ajax(url) { this.doNavigate(url, true, "loading-overlay"); } /** * Load the new page in background, will throw on errors */ load(url) { return this.prepareNavigation(url, true, true, "silent"); } reload() { this.doNavigate(this.currentUrl.V, true, "loading-screen"); } doNavigate(url, canNavigate, mode) { this.prepareNavigation(url, canNavigate, false, mode).catch(e => { this.clearLoadings(); reportError(e); }); } parseUrl(url) { if (process.env.VASILLE_TARGET === "es5" && !("URL" in this.window)) { if (!/^https?:\/\//.test(url) && url.charAt(0) !== "/") { throw new TypeError("Invalid URL"); } const a = this.window.document.createElement("a"); const query = {}; const pairs = (url.split("?")[1] || "").split("&"); pairs.forEach(pair => { const [key, value] = pair.split("=").map(decodeURIComponent); if (value) { if (key in query) { query[key].push(value); } else { query[key] = [value]; } } }); a.href = url; return [a.pathname, query, a.hash]; } else { const parsed = new URL(url, this.location.origin); const query = [...parsed.searchParams.keys()].reduce((prev, key) => { return { ...prev, [key]: parsed.searchParams.getAll(key) }; }, {}); return [parsed.pathname, query, parsed.hash]; } } async loadTarget(target, props, mode) { this.loadingUrl.V = 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"); this.currentUrl.V = props.url; if (this.location.href !== props.url && this.location.hash !== "#" + props.url) { if (process.env.VASILLE_TARGET === "es5" && !this.window.history) { this.location.hash = "#" + props.url; } else { this.window.history.pushState({}, "", props.url); } } } catch (error) { if (mode !== "silent") { this.clearNode(this.contentNode); } throw error; } finally { this.clearLoadings(); this.loadingUrl.V = 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") { if (process.env.VASILLE_TARGET === "es5" && !this.window.history) { this.window.location.hash = "#/"; } else { 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), node => { const router = new Router(window, location, node, init); Object.defineProperty(runner, "router", { get() { return router; }, }); }); return app; }