client-side-router
Version:
A client-side router for vanilla JavaScript projects.
235 lines (225 loc) • 7.32 kB
JavaScript
// 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 };