Compare commits
5 Commits
master
...
update-iso
Author | SHA1 | Date | |
---|---|---|---|
696518a12e | |||
d02b768050 | |||
11359a906f | |||
fadb797c55 | |||
b77dd89437 |
24
app.tsx
24
app.tsx
@ -1,7 +1,25 @@
|
|||||||
import * as ISO from ">able/iso-elements.tsx";
|
import {Route, useRoute, Meta} from ">able/iso-router.tsx";
|
||||||
|
|
||||||
console.log(ISO)
|
|
||||||
|
|
||||||
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>
|
||||||
|
<a href="/404">404</a>
|
||||||
|
</nav>
|
||||||
|
<Meta title="default title"/>
|
||||||
|
<Route path={[]}><p>home page!</p></Route>
|
||||||
|
<Route path={["about"]}>
|
||||||
|
<nav class="flex gap-10 p-6">
|
||||||
|
<a href="/about/more">more</a>
|
||||||
|
<a href="/about/404">more 404</a>
|
||||||
|
</nav>
|
||||||
|
<Route path={[]}><p>About page!</p></Route>
|
||||||
|
<Route path={["more"]}><p>more!</p></Route>
|
||||||
|
<Route>
|
||||||
|
<Meta title="about error!"/>
|
||||||
|
<span>couldnt find it</span>
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
|
<Route><p>404 :(</p></Route>
|
||||||
</div>;
|
</div>;
|
134
iso-router.tsx
Normal file
134
iso-router.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import * as Signal from "@preact/signals";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
///////// Metas
|
||||||
|
type MetaFields = {title?:string, description?:string}
|
||||||
|
type MetaRecord = MetaFields&{id:string, baked?:MetaFields}
|
||||||
|
const Stack = [] as MetaRecord[];
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
const StackPop =(id:string)=> {Stack.splice(Stack.findIndex( item => item.id === id ), 1); Update();}
|
||||||
|
const Update =()=> document.title = Stack.at(-1)?.baked?.title || "---";
|
||||||
|
Update();
|
||||||
|
export const useMeta=(fields:MetaFields)=>
|
||||||
|
{
|
||||||
|
const id = React.useId();
|
||||||
|
React.useEffect(()=>{
|
||||||
|
|
||||||
|
StackPush({...fields, id});
|
||||||
|
return ()=>StackPop(id);
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
export const Meta =(props:MetaFields)=> { useMeta(props); return null; }
|
||||||
|
|
||||||
|
|
||||||
|
//////// Router
|
||||||
|
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>
|
||||||
|
}
|
||||||
|
const blocked = Signal.signal(false as false|string)
|
||||||
|
const context = React.createContext({nestedDepth:0, pathIndex:0, keys:{}, blocked} as RouteContextData);
|
||||||
|
//// Create Signals
|
||||||
|
export const pageURL = Signal.signal(new URL(globalThis?.location.href || ""));
|
||||||
|
export const pagePath = Signal.signal([] as string[]);
|
||||||
|
Signal.effect(()=>{
|
||||||
|
blocked.value = false;
|
||||||
|
pagePath.value = pageURL.value.pathname.split("/").filter(part=>part!="");
|
||||||
|
});
|
||||||
|
//// 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) );
|
||||||
|
//// Rendering context
|
||||||
|
type PathFields = {pathAfter:string[], pathBefore:[], compare:typeof compare}
|
||||||
|
const compare =(pathA:string[], pathB:string[])=>
|
||||||
|
{
|
||||||
|
const emptyMatch = !pathA.length && !pathB.length;
|
||||||
|
const comparisonSize = Math.min(pathA.length, pathB.length);
|
||||||
|
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++)
|
||||||
|
{
|
||||||
|
const partPage = pathB[i];
|
||||||
|
const partRoute = pathA[i];
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
export const Route =(props:{path?:string[], children:React.ReactNode|React.ReactNode[]}):React.JSX.Element|null=>
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
const check = compare(props.path, pathAfter);
|
||||||
|
if (!check){ return null; }
|
||||||
|
|
||||||
|
blocked.value = "/"+props.path.join("/");
|
||||||
|
passOn.pathIndex += check.comparisonSize;
|
||||||
|
passOn.keys = {...keys, ...check.keys};
|
||||||
|
}
|
||||||
|
return <context.Provider value={passOn}>{props.children}</context.Provider>;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user