better useRoute hook

This commit is contained in:
Seth Trowbridge 2024-05-16 12:18:01 -04:00
parent b77dd89437
commit fadb797c55
2 changed files with 66 additions and 20 deletions

30
app.tsx
View File

@ -1,7 +1,33 @@
import * as ISO from ">able/iso-elements.tsx"; import {Route, useRoute} from ">able/iso-router.tsx";
console.log(ISO) const Tracer =()=>
{
const {nestedDepth, pathIndex} = useRoute();
return <div>Depth: {nestedDepth}, Index:{pathIndex}</div>
}
export default ()=><div> export default ()=><div>
<h1 class="p-4 bg-red-500 text-white">App</h1> <h1 class="p-4 bg-red-500 text-white">App</h1>
<nav class="flex gap-10 p-6">
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
<Route path={[]}>
<p>home page! <Tracer/></p>
</Route>
<Route path={["about"]}>
<nav class="flex gap-10 p-6">
<a href="/about/more">more</a>
</nav>
<Route path={[]}>
<p>About page! <Tracer/></p>
</Route>
<Route path={["more"]}>
<p>more!<Tracer/></p>
</Route>
</Route>
</div>; </div>;

View File

@ -19,12 +19,13 @@ export const useMeta=(fields:MetaFields)=>
} }
export const Meta =(props:MetaFields)=> { useMeta(props); return null; } export const Meta =(props:MetaFields)=> { useMeta(props); return null; }
//////// Router //////// Router
//// Create Signals //// Create Signals
export const pageURL = Signal.signal(new URL(globalThis.location.href || "")); export const pageURL = Signal.signal(new URL(globalThis?.location.href || ""));
export const pagePath = Signal.signal([] as string[]); export const pagePath = Signal.signal([] as string[]);
Signal.effect(()=> pagePath.value = pageURL.value.pathname.split("/").filter(part=>part!="")); Signal.effect(()=> pagePath.value = pageURL.value.pathname.split("/").filter(part=>part!=""));
//// Add handlers //// Add handlers
globalThis.addEventListener("click", e=> globalThis.addEventListener("click", e=>
{ {
@ -43,33 +44,28 @@ globalThis.addEventListener("click", e=>
}) })
}); });
globalThis.addEventListener("popstate", _=> pageURL.value = new URL(globalThis.location.href) ); globalThis.addEventListener("popstate", _=> pageURL.value = new URL(globalThis.location.href) );
/// Rendering context /// Rendering context
type RouteContextData = { type RouteContextData = {
/** Current nested depth of the router */ nestedDepth:number, /** Current nested depth of the router */ nestedDepth:number,
/** Index into the page path to start matching routes */ pathIndex:number, /** Index into the page path to start matching routes */ pathIndex:number,
/** Collection of keys from matched routes ("page/:key/") */ keys: Record<string, string> /** Collection of keys from matched routes ("page/:key/") */ keys: Record<string, string>,
blocked: Signal.Signal<string | false>
} }
const context = React.createContext({nestedDepth:0, pathIndex:0, keys:{}} as RouteContextData); const context = React.createContext({nestedDepth:0, pathIndex:0, keys:{}, blocked:Signal.signal(false as false|string)} as RouteContextData);
export const useRoute =()=> React.useContext(context); type PathFields = {pathAfter:string[], pathBefore:[], compare:typeof compare}
export const Route =(props:{path:string[], children:React.ReactNode|React.ReactNode[]}):React.JSX.Element|null=> const compare =(pathA:string[], pathB:string[])=>
{ {
// Match the current page url, with a route. const emptyMatch = !pathA.length && !pathB.length;
const {nestedDepth, pathIndex} = React.useContext(context); const comparisonSize = Math.min(pathA.length, pathB.length);
const pagePathRange = pagePath.value.slice(pathIndex);
const emptyMatch = !props.path.length && !pagePathRange.length;
const comparisonSize = Math.min(props.path.length, pagePath.value.length);
if(comparisonSize == 0 && emptyMatch === false) // one of the arrays is empty and one is not if(comparisonSize == 0 && emptyMatch === false) // one of the arrays is empty and one is not
{ {
return null; return null;
} }
const keys = {} as Record<string, string>; // not doing anything with this currently const keys = {} as Record<string, string>; // not doing anything with this currently
for(let i=0; i<comparisonSize; i++) for(let i=0; i<comparisonSize; i++)
{ {
const partPage = pagePathRange[i]; const partPage = pathB[i];
const partRoute = props.path[i]; const partRoute = pathA[i];
if(partPage !== partRoute) // theres a mismatch if(partPage !== partRoute) // theres a mismatch
{ {
if(partRoute?.startsWith(":")) // mismatch because variable capture, consider it a match and capture if(partRoute?.startsWith(":")) // mismatch because variable capture, consider it a match and capture
@ -83,6 +79,30 @@ export const Route =(props:{path:string[], children:React.ReactNode|React.ReactN
} }
} }
} }
return {comparisonSize, keys};
return <context.Provider value={{nestedDepth:nestedDepth+1, pathIndex:pathIndex+comparisonSize, keys}}>{props.children}</context.Provider> };
export const useRoute =()=> {
const routeContext = React.useContext(context);
const pathFull = pagePath.value;
const pathAfter = pathFull.slice(routeContext.pathIndex);
const pathBefore = pathFull.slice(0, Math.max(0, routeContext.pathIndex-1));
return { ...routeContext, pathAfter, pathBefore, compare } as PathFields&RouteContextData;
}
export const Route =(props:{path:string[], children:React.ReactNode|React.ReactNode[]}):React.JSX.Element|null=>
{
const {nestedDepth, pathIndex, pathAfter, compare, keys} = useRoute();
const check = compare(props.path, pathAfter);
if (check)
{
return <context.Provider value={{
nestedDepth:nestedDepth+1,
pathIndex:pathIndex+check.comparisonSize,
keys: {...keys, ...check.keys},
blocked:Signal.signal(false)
}}>{props.children}</context.Provider>;
}
else
{
return null;
}
} }