UNPKG

fluxel

Version:

An ultra-lightweight, high-performance library for efficient DOM building and dynamic web UIs

171 lines (170 loc) 7.59 kB
import Fluxel, { pureStateReservedKeys } from "../baseReactive.js"; export const routerEventTarget = new EventTarget(); if (typeof window !== "undefined") { window.addEventListener("popstate", (event)=>{ routerEventTarget.dispatchEvent(new CustomEvent("popstate", { detail: event })); }); window.addEventListener("hashchange", (event)=>{ routerEventTarget.dispatchEvent(new CustomEvent("popstate", { detail: event })); }); } const defaultFallback = ()=>Fluxel.div({ dataset: { fluxelRouterFallback: "1" } }); const routerInstanceStack = []; const noop = ()=>{}; export function createRouter(config, options = {}) { const basePath = options.basePath || ""; const mode = options.mode || "history"; const regexMap = []; Object.keys(config).forEach((pattern)=>{ const regex = new RegExp(`^${pattern.replace(/\/\*?$/, "").replace(RegExp("(?<cap>:[^\\s/]+)\\?\\/", "g"), "($<cap>\/)?").replace(RegExp(":(?<name>[^\\s\\?/]+)", "g"), "(?<$<name>>[^/]+)")}${pattern.endsWith("/*") ? "(/.*)?" : "/?"}$`); regexMap.push([ regex, pattern ]); }); const generateReactiveState = (query)=>{ let state = null; Fluxel.reactive(query, (_state)=>{ state = _state; }); return state; }; let onceRendered = false; let deferredPromise = null; const router = { basePath, pathname: "/", route: null, params: {}, query: null, get mode () { return mode; }, Provider (props) { return Fluxel.reactive({ children: null }, (state)=>{ const updateChildrenInternal = (route, children)=>{ onceRendered = true; if (deferredPromise) { Promise.all(deferredPromise).then(()=>{ if (!route || router.route === route) { state.children = children; } deferredPromise = null; }); return; } else { state.children = children; } }; const updateChildren = ()=>{ const url = new URL(location.href); const path = mode === "history" ? url.pathname.replace(basePath, "") : url.hash.replace(/^#/, "").replace(basePath, ""); const searchParams = new URLSearchParams(url.search); router.pathname = path; const matched = regexMap.find((regexPattern)=>regexPattern[0].test(path)); if (matched) { const regex = matched[0]; const route = matched[1]; const matchGroups = regex.exec(path).groups || {}; if (router.route === route && JSON.stringify(router.params) === JSON.stringify(matchGroups)) { Object.keys(router.query).filter((key)=>!searchParams.has(key) && !pureStateReservedKeys.includes(key)).forEach((key)=>{ router.query[key] = undefined; }); searchParams.forEach((value, key)=>{ router.query[key] = value; }); return; } router.route = route; router.params = matchGroups; const query = {}; searchParams.forEach((value, key)=>{ query[key] = value; }); router.query = generateReactiveState(query); routerInstanceStack.push(router); const children = config[route](); routerInstanceStack.pop(); if (children instanceof Promise) { var _options_fallback; children.then((resolvedChildren)=>{ config[route] = resolvedChildren; if (router.route !== route) return; routerInstanceStack.push(router); updateChildrenInternal(route, resolvedChildren()); routerInstanceStack.pop(); }); updateChildrenInternal(route, ((_options_fallback = options.fallback) === null || _options_fallback === void 0 ? void 0 : _options_fallback.call(options)) || defaultFallback()); } else { updateChildrenInternal(route, children); } } else { var _options_fallback1; router.route = null; router.params = {}; const query = {}; searchParams.forEach((value, key)=>{ query[key] = value; }); router.query = generateReactiveState(query); updateChildrenInternal(null, ((_options_fallback1 = options.fallback) === null || _options_fallback1 === void 0 ? void 0 : _options_fallback1.call(options)) || defaultFallback()); } }; routerEventTarget.addEventListener("popstate", ()=>updateChildren()); updateChildren(); return Fluxel.div({ dataset: { fluxelRouter: "1" }, classList: (props === null || props === void 0 ? void 0 : props.classList) || [], children: state.use("children", (children)=>{ var _options_fallback; return children ? children : ((_options_fallback = options.fallback) === null || _options_fallback === void 0 ? void 0 : _options_fallback.call(options)) || defaultFallback(); }) }); }); }, navigate (path, options = {}) { if (router.mode === "hash") { path = `#${router.basePath}${path}`; } globalNavigate(path, options); }, deferUpdate (process) { if (!onceRendered) { process().catch(noop); } else { deferredPromise = deferredPromise || []; deferredPromise.push(process().catch(noop)); } } }; return router; } export const useRouter = (option)=>{ const optional = (option === null || option === void 0 ? void 0 : option.optional) || false; const result = routerInstanceStack[routerInstanceStack.length - 1]; if (!result && !optional) { throw new Error("No router instance found. Make sure the component is rendered within a Router Provider."); } return result || null; }; export function globalNavigate(path, options = {}) { const replace = options.replace || false; if (replace) { window.history.replaceState({}, "", path); } else { window.history.pushState({}, "", path); } routerEventTarget.dispatchEvent(new CustomEvent("popstate")); }