diff options
Diffstat (limited to 'packages/demobank-ui/src/route.ts')
-rw-r--r-- | packages/demobank-ui/src/route.ts | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/packages/demobank-ui/src/route.ts b/packages/demobank-ui/src/route.ts new file mode 100644 index 000000000..d54f9be83 --- /dev/null +++ b/packages/demobank-ui/src/route.ts @@ -0,0 +1,167 @@ +import { createHashHistory } from "history"; +import { h as create, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +const history = createHashHistory(); + +type PageDefinition<DynamicPart extends Record<string, string>> = { + pattern: string; + (params: DynamicPart): string; +}; + +function replaceAll( + pattern: string, + vars: Record<string, string>, + values: Record<string, string>, +): string { + let result = pattern; + for (const v in vars) { + result = result.replace(vars[v], !values[v] ? "" : values[v]); + } + return result; +} + +export function pageDefinition<T extends Record<string, string>>( + pattern: string, +): PageDefinition<T> { + const patternParams = pattern.match(/(:[\w?]*)/g); + if (!patternParams) + throw Error( + `page definition pattern ${pattern} doesn't have any parameter`, + ); + + const vars = patternParams.reduce((prev, cur) => { + const pName = cur.match(/(\w+)/g); + + //skip things like :? in the path pattern + if (!pName || !pName[0]) return prev; + const name = pName[0]; + return { ...prev, [name]: cur }; + }, {} as Record<string, string>); + + const f = (values: T): string => replaceAll(pattern, vars, values); + f.pattern = pattern; + return f; +} + +export type PageEntry<T = unknown> = T extends Record<string, string> + ? { + url: PageDefinition<T>; + view: (props: T) => VNode; + } + : T extends unknown + ? { + url: string; + view: (props: {}) => VNode; + } + : never; + +export function Router({ + pageList, + onNotFound, +}: { + pageList: Array<PageEntry<any>>; + onNotFound: () => VNode; +}): VNode { + const current = useCurrentLocation(pageList); + if (current !== undefined) { + return create(current.page.view, current.values); + } + return onNotFound(); +} + +type Location = { + page: PageEntry<any>; + path: string; + values: Record<string, string>; +}; +export function useCurrentLocation(pageList: Array<PageEntry<any>>) { + const [currentLocation, setCurrentLocation] = useState<Location>(); + /** + * Search path in the pageList + * get the values from the path found + * add params from searchParams + * + * @param path + * @param params + */ + function doSync(path: string, params: URLSearchParams) { + let result: typeof currentLocation; + for (let idx = 0; idx < pageList.length; idx++) { + const page = pageList[idx]; + if (typeof page.url === "string") { + if (page.url === path) { + const values: Record<string, string> = {}; + params.forEach((v, k) => { + values[k] = v; + }); + result = { page, values, path }; + break; + } + } else { + const values = doestUrlMatchToRoute(path, page.url.pattern); + if (values !== undefined) { + params.forEach((v, k) => { + values[k] = v; + }); + result = { page, values, path }; + break; + } + } + } + setCurrentLocation(result); + } + useEffect(() => { + doSync(window.location.hash, new URLSearchParams(window.location.search)); + return history.listen(() => { + doSync(window.location.hash, new URLSearchParams(window.location.search)); + }); + }, []); + return currentLocation; +} + +function doestUrlMatchToRoute( + url: string, + route: string, +): undefined | Record<string, string> { + const paramsPattern = /(?:\?([^#]*))?$/; + // const paramsPattern = /(?:\?([^#]*))?(#.*)?$/; + const params = url.match(paramsPattern); + const urlWithoutParams = url.replace(paramsPattern, ""); + + const result: Record<string, string> = {}; + if (params && params[1]) { + const paramList = params[1].split("&"); + for (let i = 0; i < paramList.length; i++) { + const idx = paramList[i].indexOf("="); + const name = paramList[i].substring(0, idx); + const value = paramList[i].substring(idx + 1); + result[decodeURIComponent(name)] = decodeURIComponent(value); + } + } + const urlSeg = urlWithoutParams.split("/"); + const routeSeg = route.split("/"); + let max = Math.max(urlSeg.length, routeSeg.length); + for (let i = 0; i < max; i++) { + if (routeSeg[i] && routeSeg[i].charAt(0) === ":") { + const param = routeSeg[i].replace(/(^:|[+*?]+$)/g, ""); + + const flags = (routeSeg[i].match(/[+*?]+$/) || EMPTY)[0] || ""; + const plus = ~flags.indexOf("+"); + const star = ~flags.indexOf("*"); + const val = urlSeg[i] || ""; + + if (!val && !star && (flags.indexOf("?") < 0 || plus)) { + return undefined; + } + result[param] = decodeURIComponent(val); + if (plus || star) { + result[param] = urlSeg.slice(i).map(decodeURIComponent).join("/"); + break; + } + } else if (routeSeg[i] !== urlSeg[i]) { + return undefined; + } + } + return result; +} +const EMPTY: Record<string, string> = {}; |