UNPKG

@xan105/vanilla-router

Version:

Simple Vanilla JS router based on the Navigation API and URLPattern API

143 lines (118 loc) 3.81 kB
/* Copyright (c) Anthony Beaumont This source code is licensed under the MIT License found in the LICENSE file in the root directory of this source tree. */ export class Router extends EventTarget { #routes; #notFound; constructor(option = {}){ super(); this.#routes = Object.create(null); this.#notFound = Symbol("404"); this.autoFire = option?.autoFire ?? true; this.sensitive = option?.sensitive ?? true; this.autoFocus = option?.autoFocus ?? true; this.autoScroll = option?.autoScroll ?? true; this.deferredCommit = option?.deferredCommit ?? false; } get routes(){ return Reflect.ownKeys(this.#routes); } get current(){ return navigation.currentEntry; } get history(){ return navigation.entries(); } back(){ if(navigation.canGoBack) return navigation.back(); } forward(){ if(navigation.canGoForward) return navigation.forward(); } navigate(url, options){ return navigation.navigate(url, options); } on(path, handler, options = {}) { if(typeof handler !== "function") return this; if(typeof path === "string" && path.length > 0){ this.#routes[path] = { handler, options }; } else if(path === 404){ this.#routes[this.#notFound] = { handler, options }; } return this; } off(path){ if(typeof path === "string" && path.length > 0){ delete this.#routes[path]; } else if(path === 404){ delete this.#routes[this.#notFound]; } return this; } #match(path){ if(this.sensitive === true) { if (Object.hasOwn(this.#routes, path)){ return this.#routes[path]; } } for (const [route, { handler, options }] of Object.entries(this.#routes)) { const pattern = new URLPattern({ pathname: route }, { ignoreCase: this.sensitive === false }); const match = pattern.exec({ pathname: path }); if(match){ return { handler, options, param: match.pathname.groups }; } else { continue; } } return this.#routes[this.#notFound]; } listen(){ navigation.addEventListener("navigateerror", (e) => { const url = new URL(e.currentTarget.currentEntry.url); this.dispatchEvent(new CustomEvent("error", { detail: { error: e.message, url } })); }); navigation.addEventListener("navigatesuccess", (e) => { const url = new URL(e.currentTarget.currentEntry.url); this.dispatchEvent(new CustomEvent("did-navigate", { detail: { url } })); }); navigation.addEventListener("navigate", (e) => { if ( !e.canIntercept || e.hashChange || e.downloadRequest !== null || e.formData ) return; const url = new URL(e.destination.url); const { handler, options, param } = this.#match(url.pathname) ?? {}; if(!handler) { this.dispatchEvent(new CustomEvent("error", { detail: { error: "No handler found !", url } })); return; } this.dispatchEvent(new CustomEvent("will-navigate", { detail: { url } })); e.intercept({ focusReset: (options?.autoFocus ?? this.autoFocus) === true ? "after-transition" : "manual", scroll: (options?.autoScroll ?? this.autoScroll) === true ? "after-transition" : "manual", commit: (options?.deferredCommit ?? this.deferredCommit) === true ? "after-transition" : "immediate", handler: handler.bind(this, e, url, param ?? {}) }); }); //Navigation API doesn't trigger "navigate" on page first load if(this.autoFire === true) { this.navigate(location.pathname, { history: "replace"}); } return this; } } export { updateMetadata } from "./metadata.js"