vasille-router
Version:
The first Developer eXperience Orientated front-end framework (router library).
129 lines (128 loc) • 4.61 kB
JavaScript
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;
}