able-baker/iso-router.tsx

134 lines
4.6 KiB
TypeScript
Raw Permalink Normal View History

2024-05-07 20:05:38 -04:00
import * as Signal from "@preact/signals";
import * as React from "react";
///////// Metas
2024-05-17 17:33:19 -04:00
type MetaFields = {title?:string, description?:string}
type MetaRecord = MetaFields&{id:string, baked?:MetaFields}
2024-05-07 20:05:38 -04:00
const Stack = [] as MetaRecord[];
2024-05-17 17:33:19 -04:00
const StackPush =(m:MetaRecord)=>
{
m.baked = {...(Stack.at(-1)?.baked || {}), ...m};
/*
const previous = Stack.at(-1)?.baked;
if(previous)
{
Object.entries(previous).forEach(([key, prevValue], i)=>
{
const fields = m.baked as Record<string, string>;
const currValue = fields[key];
if(currValue)
{
}
fields[key] = prevValue + concat + currValue;
})
}
*/
Stack.push(m); Update();
}
2024-05-07 20:05:38 -04:00
const StackPop =(id:string)=> {Stack.splice(Stack.findIndex( item => item.id === id ), 1); Update();}
2024-05-17 17:33:19 -04:00
const Update =()=> document.title = Stack.at(-1)?.baked?.title || "---";
2024-05-07 20:05:38 -04:00
Update();
export const useMeta=(fields:MetaFields)=>
{
const id = React.useId();
React.useEffect(()=>{
2024-05-17 17:33:19 -04:00
2024-05-07 20:05:38 -04:00
StackPush({...fields, id});
return ()=>StackPop(id);
}, []);
}
export const Meta =(props:MetaFields)=> { useMeta(props); return null; }
2024-05-16 12:18:01 -04:00
2024-05-07 20:05:38 -04:00
//////// Router
2024-05-16 14:30:04 -04:00
type RouteContextData = {
/** Current nested depth of the router */ nestedDepth:number,
/** Index into the page path to start matching routes */ pathIndex:number,
/** Collection of keys from matched routes ("page/:key/") */ keys: Record<string, string>,
blocked: Signal.Signal<string | false>
}
2024-05-18 12:41:44 -04:00
const blocked = Signal.signal(false as false|string)
const context = React.createContext({nestedDepth:0, pathIndex:0, keys:{}, blocked} as RouteContextData);
2024-05-07 20:05:38 -04:00
//// Create Signals
2024-05-16 12:18:01 -04:00
export const pageURL = Signal.signal(new URL(globalThis?.location.href || ""));
2024-05-07 20:05:38 -04:00
export const pagePath = Signal.signal([] as string[]);
2024-05-16 14:30:04 -04:00
Signal.effect(()=>{
2024-05-18 12:41:44 -04:00
blocked.value = false;
2024-05-16 14:30:04 -04:00
pagePath.value = pageURL.value.pathname.split("/").filter(part=>part!="");
});
2024-05-07 20:05:38 -04:00
//// Add handlers
globalThis.addEventListener("click", e=>
{
(e.composedPath() as HTMLAnchorElement[]).find((step)=>{
if(step.href)
{
const url = new URL(step.href);
if(url.origin == document.location.origin)
{
e.preventDefault();
history.pushState({}, "", url);
pageURL.value = url;
}
return true;
}
})
});
globalThis.addEventListener("popstate", _=> pageURL.value = new URL(globalThis.location.href) );
2024-05-16 14:30:04 -04:00
//// Rendering context
2024-05-16 12:18:01 -04:00
type PathFields = {pathAfter:string[], pathBefore:[], compare:typeof compare}
const compare =(pathA:string[], pathB:string[])=>
2024-05-07 20:05:38 -04:00
{
2024-05-16 12:18:01 -04:00
const emptyMatch = !pathA.length && !pathB.length;
const comparisonSize = Math.min(pathA.length, pathB.length);
2024-05-07 20:05:38 -04:00
if(comparisonSize == 0 && emptyMatch === false) // one of the arrays is empty and one is not
{
return null;
}
const keys = {} as Record<string, string>; // not doing anything with this currently
for(let i=0; i<comparisonSize; i++)
{
2024-05-16 12:18:01 -04:00
const partPage = pathB[i];
const partRoute = pathA[i];
2024-05-07 20:05:38 -04:00
if(partPage !== partRoute) // theres a mismatch
{
if(partRoute?.startsWith(":")) // mismatch because variable capture, consider it a match and capture
{
const key = partRoute?.substring(1);
key && (keys[key] = partPage);
}
else if(partRoute !== "*") // otherwise quit if its not a wildcard section
{
return null;
}
}
}
2024-05-16 12:18:01 -04:00
return {comparisonSize, keys};
};
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;
}
2024-05-16 14:30:04 -04:00
export const Route =(props:{path?:string[], children:React.ReactNode|React.ReactNode[]}):React.JSX.Element|null=>
2024-05-16 12:18:01 -04:00
{
2024-05-16 14:30:04 -04:00
const {nestedDepth, pathIndex, pathAfter, compare, keys, blocked} = useRoute();
if(blocked.peek()){ return null; }
const passOn:RouteContextData = { nestedDepth:nestedDepth+1, pathIndex, keys, blocked:Signal.signal(false) }
if(props.path)
2024-05-16 12:18:01 -04:00
{
2024-05-16 14:30:04 -04:00
const check = compare(props.path, pathAfter);
if (!check){ return null; }
blocked.value = "/"+props.path.join("/");
passOn.pathIndex += check.comparisonSize;
passOn.keys = {...keys, ...check.keys};
2024-05-16 12:18:01 -04:00
}
2024-05-16 14:30:04 -04:00
return <context.Provider value={passOn}>{props.children}</context.Provider>;
2024-05-07 20:05:38 -04:00
}