vasille-router
Version:
The framework designed to build bulletproof frontends (router library).
172 lines (171 loc) • 6.17 kB
JavaScript
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;
}