fluxel
Version:
An ultra-lightweight, high-performance library for efficient DOM building and dynamic web UIs
171 lines (170 loc) • 7.59 kB
JavaScript
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"));
}