UNPKG

client-side-router

Version:

A client-side router for vanilla JavaScript projects.

235 lines (225 loc) 7.32 kB
// src/context/RequestKind.ts var RequestKind = /* @__PURE__ */ ((RequestKind2) => { RequestKind2["Normal"] = "normal"; RequestKind2["PageLoad"] = "page-load"; RequestKind2["PopState"] = "popstate"; return RequestKind2; })(RequestKind || {}); var RequestKind_default = RequestKind; // src/context/NavRequest.ts var NavRequest = class { kind; pathName; constructor(kind, pathName) { this.kind = kind; this.pathName = pathName; } isPageLoadRequest() { return this.kind === RequestKind_default.PageLoad; } isPopStateRequest() { return this.kind === RequestKind_default.PopState; } /** * @returns `true` if this request wasn't made on page load or during a popstate event. */ isNormalRequest() { return this.kind === RequestKind_default.Normal; } }; // src/EventBus.ts var EventTypes = { NavRequest: "router-nav-request", NavComplete: "router-nav-complete" }; var eventTarget = new EventTarget(); var [emitNavRequest, onNavRequest] = createEventHandlers(EventTypes.NavRequest); var [emitNavComplete, onNavComplete] = createEventHandlers(EventTypes.NavComplete); function createEventHandlers(eventType) { const emit = (detail) => { eventTarget.dispatchEvent(new CustomEvent(eventType, { detail })); }; const listen = (listener) => { eventTarget.addEventListener(eventType, (e) => { listener(e.detail); }); }; return [emit, listen]; } function emitNavRequestOf(kind, pathName) { emitNavRequest(new NavRequest(kind, pathName)); } var EventBus_default = { emitNavRequest, emitNavRequestOf, emitNavComplete, onNavRequest, onNavComplete }; // src/context/NavResponse.ts var NavResponse = class { constructor(routeName, params, component) { this.routeName = routeName; this.params = params; this.component = component; } static DEFAULT_RESPONSE = new this("404", {}, () => { document.title = "404 page not found"; return document.title; }); static defaultResponse() { return this.DEFAULT_RESPONSE; } node() { return this.component(this.params); } }; // src/RouteLoader/name-generator.ts function createNameGenerator() { let id = 0; return () => `_DEFAULT_ROUTE_NAME_${id++}`; } // src/RouteLoader/RouteDefinition.ts var RouteDefinition = class _RouteDefinition { static PARAM_REGEX = /\/:([^/]+)/g; path; regex; component; constructor(path, component) { this.path = path; this.regex = new RegExp(`^${path.replace(_RouteDefinition.PARAM_REGEX, "/(?<$1>[^/]+)")}$`); this.component = component; } realPathName(params) { return this.path.replace(_RouteDefinition.PARAM_REGEX, (_, p1) => { return "/" + (params[p1] ?? ""); }); } componentAndParams(pathName) { const matchArray = pathName.match(this.regex); return matchArray ? [this.component, matchArray.groups ?? {}] : [null, null]; } }; // src/RouteLoader/RouteLoader.ts var nextName = createNameGenerator(); var definitions = /* @__PURE__ */ new Map(); var responseCache = /* @__PURE__ */ new Map(); function addRouteDefinition(path, component, routeName) { const routeDefinition = new RouteDefinition(path, component); definitions.set(routeName ?? nextName(), routeDefinition); } function createRequestFromRouteName(routeName, params) { const routeDefinition = definitions.get(routeName); if (!routeDefinition) throw new Error(`No route was found for name "${routeName}".`); const pathName = routeDefinition.realPathName(params); return new NavRequest(RequestKind_default.Normal, pathName); } function getResponse(pathName) { const cachedResponse = responseCache.get(pathName); if (cachedResponse) return cachedResponse; const response = getResponseUncached(pathName); responseCache.set(pathName, response); return response; } function getResponseUncached(pathName) { for (const [routeName, routeDefinition] of definitions.entries()) { const [component, params] = routeDefinition.componentAndParams(pathName); if (component) return new NavResponse(routeName, params, component); } return NavResponse.defaultResponse(); } var RouteLoader_default = { addRouteDefinition, createRequestFromRouteName, getResponse }; // src/RouterOutlet.ts var RouterOutlet = class extends HTMLElement { _basePath; _handleInternalLinks; constructor(basePath, handleInternalLinks) { super(); this._basePath = basePath; this._handleInternalLinks = handleInternalLinks; this.style.display = "contents"; } get _currentPathName() { return this._removeBasePath(location.pathname); } connectedCallback() { window.addEventListener("popstate", () => { EventBus_default.emitNavRequestOf(RequestKind_default.PopState, this._currentPathName); }); if (this._handleInternalLinks) document.addEventListener("click", (e) => { this._handleAnchorClick(e); }); EventBus_default.onNavRequest(async (request) => { const response = RouteLoader_default.getResponse(request.pathName); switch (request.kind) { case RequestKind_default.PageLoad: case RequestKind_default.PopState: await this._updateChildren(response); break; case RequestKind_default.Normal: if (request.pathName !== this._currentPathName) { history.pushState({}, "", this._basePath + request.pathName); await this._updateChildren(response); } } this._completeRequest(request, response); }); EventBus_default.emitNavRequestOf(RequestKind_default.PageLoad, this._currentPathName); } _removeBasePath(pathName) { return pathName.slice(this._basePath.length); } _completeRequest(request, response) { EventBus_default.emitNavComplete({ requestKind: request.kind, pathName: request.pathName, routeName: response.routeName, params: response.params, component: response.component }); } _handleAnchorClick(e) { if (!(e.target instanceof HTMLAnchorElement) || e.ctrlKey) return; const url = new URL(e.target.href); if (url.origin !== location.origin) return; e.preventDefault(); const pathName = this._removeBasePath(url.pathname); if (pathName !== this._currentPathName) EventBus_default.emitNavRequestOf(RequestKind_default.Normal, pathName); } async _updateChildren(response) { const routerChild = await response.node(); this.replaceChildren(routerChild); } }; customElements.define("router-outlet", RouterOutlet); // src/navigation.ts function Router({ basePath = "", onNavStarted, onNavComplete: onNavComplete2, internalLinks = false }) { if (onNavStarted) EventBus_default.onNavRequest(onNavStarted); if (onNavComplete2) EventBus_default.onNavComplete(onNavComplete2); return new RouterOutlet(basePath, internalLinks); } function Route({ path, component, name }) { RouteLoader_default.addRouteDefinition(path, component, name); return null; } function navigate(path) { EventBus_default.emitNavRequestOf(RequestKind_default.Normal, path); } function navigateToRoute(routeName, params = {}) { const request = RouteLoader_default.createRequestFromRouteName(routeName, params); EventBus_default.emitNavRequest(request); } export { Route, Router, navigate, navigateToRoute };