@solidjs/router
Advanced tools
Comparing version 0.9.1 to 0.10.0-beta.0
import type { Component, JSX } from "solid-js"; | ||
import type { Location, LocationChangeSignal, MatchFilters, Navigator, RouteDataFunc, RouteDefinition, RouterIntegration } from "./types"; | ||
import type { Location, LocationChangeSignal, MatchFilters, Navigator, RouteLoadFunc, RouterIntegration, RouteSectionProps } from "./types"; | ||
declare module "solid-js" { | ||
@@ -9,3 +9,3 @@ namespace JSX { | ||
replace?: boolean; | ||
link?: boolean; | ||
preload?: boolean; | ||
} | ||
@@ -16,5 +16,4 @@ } | ||
base?: string; | ||
data?: RouteDataFunc; | ||
root?: Component<RouteSectionProps>; | ||
children: JSX.Element; | ||
out?: object; | ||
} & ({ | ||
@@ -28,23 +27,10 @@ url?: never; | ||
export declare const Router: (props: RouterProps) => JSX.Element; | ||
export interface RoutesProps { | ||
base?: string; | ||
children: JSX.Element; | ||
} | ||
export declare const Routes: (props: RoutesProps) => JSX.Element; | ||
export declare const useRoutes: (routes: RouteDefinition | RouteDefinition[] | Readonly<RouteDefinition[]>, base?: string) => () => JSX.Element; | ||
export type RouteProps<S extends string> = { | ||
path: S | S[]; | ||
path?: S | S[]; | ||
children?: JSX.Element; | ||
data?: RouteDataFunc; | ||
load?: RouteLoadFunc; | ||
matchFilters?: MatchFilters<S>; | ||
} & ({ | ||
element?: never; | ||
component: Component; | ||
} | { | ||
component?: never; | ||
element?: JSX.Element; | ||
preload?: () => void; | ||
}); | ||
component?: Component; | ||
}; | ||
export declare const Route: <S extends string>(props: RouteProps<S>) => JSX.Element; | ||
export declare const Outlet: () => JSX.Element; | ||
export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> { | ||
@@ -51,0 +37,0 @@ href: string; |
@@ -5,7 +5,7 @@ /*@refresh skip*/ | ||
import { pathIntegration, staticIntegration } from "./integration"; | ||
import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj, useHref, useLocation, useNavigate, useResolvedPath, useRoute, useRouter } from "./routing"; | ||
import { joinPaths, normalizePath, createMemoObject } from "./utils"; | ||
import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj, useHref, useLocation, useNavigate, useResolvedPath } from "./routing"; | ||
import { normalizePath, createMemoObject } from "./utils"; | ||
export const Router = (props) => { | ||
let e; | ||
const { source, url, base, data, out } = props; | ||
const { source, url, base } = props; | ||
const integration = source || | ||
@@ -15,11 +15,16 @@ (isServer | ||
: pathIntegration()); | ||
const routerState = createRouterContext(integration, base, data, out); | ||
return (<RouterContextObj.Provider value={routerState}>{props.children}</RouterContextObj.Provider>); | ||
const routeDefs = children(() => props.root | ||
? { | ||
component: props.root, | ||
children: props.children | ||
} | ||
: props.children); | ||
const branches = createMemo(() => createBranches(routeDefs(), props.base || "")); | ||
const routerState = createRouterContext(integration, branches, base); | ||
return (<RouterContextObj.Provider value={routerState}> | ||
<Routes routerState={routerState} branches={branches()}/> | ||
</RouterContextObj.Provider>); | ||
}; | ||
export const Routes = (props) => { | ||
const router = useRouter(); | ||
const parentRoute = useRoute(); | ||
const routeDefs = children(() => props.children); | ||
const branches = createMemo(() => createBranches(routeDefs(), joinPaths(parentRoute.pattern, props.base || ""), Outlet)); | ||
const matches = createMemo(() => getRouteMatches(branches(), router.location.pathname)); | ||
function Routes(props) { | ||
const matches = createMemo(() => getRouteMatches(props.branches, props.routerState.location.pathname)); | ||
const params = createMemoObject(() => { | ||
@@ -33,10 +38,2 @@ const m = matches(); | ||
}); | ||
if (router.out) { | ||
router.out.matches.push(matches().map(({ route, path, params }) => ({ | ||
originalPath: route.originalPath, | ||
pattern: route.pattern, | ||
path, | ||
params | ||
}))); | ||
} | ||
const disposers = []; | ||
@@ -60,3 +57,3 @@ let root; | ||
disposers[i] = dispose; | ||
next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i], params); | ||
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => matches()[i], params); | ||
}); | ||
@@ -75,6 +72,8 @@ } | ||
</Show>); | ||
} | ||
const createOutlet = (child) => { | ||
return () => (<Show when={child()} keyed> | ||
{child => <RouteContextObj.Provider value={child}>{child.outlet()}</RouteContextObj.Provider>} | ||
</Show>); | ||
}; | ||
export const useRoutes = (routes, base) => { | ||
return () => <Routes base={base}>{routes}</Routes>; | ||
}; | ||
export const Route = (props) => { | ||
@@ -88,8 +87,2 @@ const childRoutes = children(() => props.children); | ||
}; | ||
export const Outlet = () => { | ||
const route = useRoute(); | ||
return (<Show when={route.child} keyed> | ||
{child => <RouteContextObj.Provider value={child}>{child.outlet()}</RouteContextObj.Provider>} | ||
</Show>); | ||
}; | ||
export function A(props) { | ||
@@ -116,3 +109,3 @@ props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props); | ||
}); | ||
return (<a link {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{ | ||
return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{ | ||
...(props.class && { [props.class]: true }), | ||
@@ -119,0 +112,0 @@ [props.inactiveClass]: !isActive(), |
export * from "./components"; | ||
export * from "./integration"; | ||
export * from "./lifecycle"; | ||
export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing"; | ||
export { useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing"; | ||
export { mergeSearchString as _mergeSearchString } from "./utils"; | ||
export type { Location, LocationChange, LocationChangeSignal, NavigateOptions, Navigator, OutputMatch, Params, RouteDataFunc, RouteDataFuncArgs, RouteDefinition, RouterIntegration, RouterOutput, RouterUtils, SetParams, BeforeLeaveEventArgs } from "./types"; | ||
export * from "./data"; | ||
export type { Location, LocationChange, LocationChangeSignal, NavigateOptions, Navigator, OutputMatch, Params, RouteLoadFunc, RouteLoadFuncArgs, RouteDefinition, RouterIntegration, RouterOutput, RouterUtils, SetParams, BeforeLeaveEventArgs } from "./types"; |
import { isServer, delegateEvents, getRequestEvent, createComponent as createComponent$1, spread, mergeProps as mergeProps$1, template } from 'solid-js/web'; | ||
import { createSignal, onCleanup, getOwner, runWithOwner, createMemo, createContext, useContext, untrack, createRenderEffect, createComponent, on, startTransition, resetErrorBoundaries, children, createRoot, Show, mergeProps, splitProps } from 'solid-js'; | ||
import { createSignal, onCleanup, getOwner, runWithOwner, createMemo, createContext, useContext, untrack, createRenderEffect, on, startTransition, createComponent, resetErrorBoundaries, children, createRoot, Show, mergeProps, splitProps, createResource, sharedConfig, $TRACK } from 'solid-js'; | ||
import { createStore, reconcile } from 'solid-js/store'; | ||
@@ -204,2 +205,3 @@ function bindEvent(target, type, handler) { | ||
const trimPathRegex = /^\/+|(\/)\/+$/g; | ||
const redirectStatusCodes = new Set([204, 301, 302, 303, 307, 308]); | ||
function normalizePath(path, omitSlash = false) { | ||
@@ -357,4 +359,3 @@ const s = path.replace(trimPathRegex, "$1"); | ||
const useRouter = () => invariant(useContext(RouterContextObj), "Make sure your app is wrapped in a <Router />"); | ||
let TempRoute; | ||
const useRoute = () => TempRoute || useContext(RouteContextObj) || useRouter().base; | ||
const useRoute = () => useContext(RouteContextObj) || useRouter().base; | ||
const useResolvedPath = path => { | ||
@@ -385,3 +386,2 @@ const route = useRoute(); | ||
const useParams = () => useRoute().params; | ||
const useRouteData = () => useRoute().data; | ||
const useSearchParams = () => { | ||
@@ -408,6 +408,6 @@ const location = useLocation(); | ||
}; | ||
function createRoutes(routeDef, base = "", fallback) { | ||
function createRoutes(routeDef, base = "") { | ||
const { | ||
component, | ||
data, | ||
load, | ||
children | ||
@@ -418,10 +418,4 @@ } = routeDef; | ||
key: routeDef, | ||
element: component ? () => createComponent(component, {}) : () => { | ||
const { | ||
element | ||
} = routeDef; | ||
return element === undefined && fallback ? createComponent(fallback, {}) : element; | ||
}, | ||
preload: routeDef.component ? component.preload : routeDef.preload, | ||
data | ||
component, | ||
load | ||
}; | ||
@@ -466,8 +460,9 @@ return asArray(routeDef.path).reduce((acc, path) => { | ||
} | ||
function createBranches(routeDef, base = "", fallback, stack = [], branches = []) { | ||
function createBranches(routeDef, base = "", stack = [], branches = []) { | ||
const routeDefs = asArray(routeDef); | ||
for (let i = 0, len = routeDefs.length; i < len; i++) { | ||
const def = routeDefs[i]; | ||
if (def && typeof def === "object" && def.hasOwnProperty("path")) { | ||
const routes = createRoutes(def, base, fallback); | ||
if (def && typeof def === "object") { | ||
if (!def.hasOwnProperty("path")) def.path = ""; | ||
const routes = createRoutes(def, base); | ||
for (const route of routes) { | ||
@@ -477,3 +472,3 @@ stack.push(route); | ||
if (def.children && !isEmptyArray) { | ||
createBranches(def.children, route.pattern, fallback, stack, branches); | ||
createBranches(def.children, route.pattern, stack, branches); | ||
} else { | ||
@@ -536,3 +531,11 @@ const branch = createBranch([...stack], branches.length); | ||
} | ||
function createRouterContext(integration, base = "", data, out) { | ||
const actions = new Map(); | ||
function registerAction(url, fn) { | ||
actions.set(url, fn); | ||
} | ||
let intent; | ||
function getIntent() { | ||
return intent; | ||
} | ||
function createRouterContext(integration, getBranches, base = "") { | ||
const { | ||
@@ -545,7 +548,4 @@ signal: [source, setSource], | ||
const beforeLeave = utils.beforeLeave || createBeforeLeave(); | ||
let submissions = []; | ||
const basePath = resolvePath("", base); | ||
const output = isServer && out ? Object.assign(out, { | ||
matches: [], | ||
url: undefined | ||
}) : undefined; | ||
if (basePath === undefined) { | ||
@@ -582,15 +582,2 @@ throw new Error(`${basePath} is not a valid base path`); | ||
}; | ||
if (data) { | ||
try { | ||
TempRoute = baseRoute; | ||
baseRoute.data = data({ | ||
data: undefined, | ||
params: {}, | ||
location, | ||
navigate: navigatorFactory(baseRoute) | ||
}); | ||
} finally { | ||
TempRoute = undefined; | ||
} | ||
} | ||
function navigateFromRoute(route, to, options) { | ||
@@ -627,5 +614,2 @@ // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths | ||
if (isServer) { | ||
if (output) { | ||
output.url = resolvedTo; | ||
} | ||
const e = getRequestEvent(); | ||
@@ -647,2 +631,3 @@ e && (e.response = Response.redirect(resolvedTo, 302)); | ||
start(() => { | ||
intent = "navigate"; | ||
setReference(resolvedTo); | ||
@@ -653,2 +638,3 @@ setState(nextState); | ||
if (referrers.length === len) { | ||
intent = undefined; | ||
navigateEnd({ | ||
@@ -691,4 +677,7 @@ value: resolvedTo, | ||
start(() => { | ||
intent = "native"; | ||
setReference(value); | ||
setState(state); | ||
}).then(() => { | ||
intent = undefined; | ||
}); | ||
@@ -699,12 +688,24 @@ } | ||
if (!isServer) { | ||
function handleAnchorClick(evt) { | ||
let preloadTimeout = {}; | ||
function isSvg(el) { | ||
return el.namespaceURI === "http://www.w3.org/2000/svg"; | ||
} | ||
function handleAnchor(evt) { | ||
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return; | ||
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A"); | ||
if (!a || !a.hasAttribute("link")) return; | ||
const href = a.href; | ||
if (a.target || !href && !a.hasAttribute("state")) return; | ||
if (!a) return; | ||
const svg = isSvg(a); | ||
const href = svg ? a.href.baseVal : a.href; | ||
const target = svg ? a.target.baseVal : a.target; | ||
if (target || !href && !a.hasAttribute("state")) return; | ||
const rel = (a.getAttribute("rel") || "").split(/\s+/); | ||
if (a.hasAttribute("download") || rel && rel.includes("external")) return; | ||
const url = new URL(href); | ||
const url = svg ? new URL(href, document.baseURI) : new URL(href); | ||
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return; | ||
return [a, url]; | ||
} | ||
function handleAnchorClick(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) return; | ||
const [a, url] = res; | ||
const to = parsePath(url.pathname + url.search + url.hash); | ||
@@ -720,11 +721,87 @@ const state = a.getAttribute("state"); | ||
} | ||
function doPreload(a, path) { | ||
const preload = a.getAttribute("preload") !== "false"; | ||
const matches = getRouteMatches(getBranches(), path); | ||
const prevIntent = intent; | ||
intent = "preload"; | ||
for (let match in matches) { | ||
const { | ||
route, | ||
params | ||
} = matches[match]; | ||
route.component && route.component.preload && route.component.preload(); | ||
preload && route.load && route.load({ | ||
params, | ||
location | ||
}); | ||
} | ||
intent = prevIntent; | ||
} | ||
function handleAnchorPreload(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) return; | ||
const [a, url] = res; | ||
if (!preloadTimeout[url.pathname]) doPreload(a, url.pathname); | ||
} | ||
function handleAnchorIn(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) return; | ||
const [a, url] = res; | ||
if (preloadTimeout[url.pathname]) return; | ||
preloadTimeout[url.pathname] = setTimeout(() => { | ||
doPreload(a, url.pathname); | ||
delete preloadTimeout[url.pathname]; | ||
}, 50); | ||
} | ||
function handleAnchorOut(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) return; | ||
const [, url] = res; | ||
if (preloadTimeout[url.pathname]) { | ||
clearTimeout(preloadTimeout[url.pathname]); | ||
delete preloadTimeout[url.pathname]; | ||
} | ||
} | ||
function handleFormSubmit(evt) { | ||
let actionRef = evt.submitter && evt.submitter.getAttribute("formaction") || evt.target.action; | ||
if (actionRef && actionRef.startsWith("action:")) { | ||
const data = new FormData(evt.target); | ||
actions.get(actionRef.slice(7))(data); | ||
evt.preventDefault(); | ||
} | ||
} | ||
// ensure delegated events run first | ||
delegateEvents(["click"]); | ||
// ensure delegated event run first | ||
delegateEvents(["click", "submit"]); | ||
document.addEventListener("click", handleAnchorClick); | ||
onCleanup(() => document.removeEventListener("click", handleAnchorClick)); | ||
document.addEventListener("mouseover", handleAnchorIn); | ||
document.addEventListener("mouseout", handleAnchorOut); | ||
document.addEventListener("focusin", handleAnchorPreload); | ||
document.addEventListener("touchstart", handleAnchorPreload); | ||
document.addEventListener("submit", handleFormSubmit); | ||
onCleanup(() => { | ||
document.removeEventListener("click", handleAnchorClick); | ||
document.removeEventListener("mouseover", handleAnchorIn); | ||
document.removeEventListener("mouseout", handleAnchorOut); | ||
document.removeEventListener("focusin", handleAnchorPreload); | ||
document.removeEventListener("touchstart", handleAnchorPreload); | ||
document.removeEventListener("submit", handleFormSubmit); | ||
}); | ||
} else { | ||
function initFromFlash(params) { | ||
let param = params.form ? JSON.parse(params.form) : null; | ||
if (!param || !param.result) { | ||
return []; | ||
} | ||
const input = new Map(param.entries); | ||
return [{ | ||
url: param.url, | ||
result: param.error ? new Error(param.result.message) : param.result, | ||
input: input | ||
}]; | ||
} | ||
submissions = initFromFlash(location.query); | ||
} | ||
return { | ||
base: baseRoute, | ||
out: output, | ||
location, | ||
@@ -735,29 +812,29 @@ isRouting, | ||
navigatorFactory, | ||
beforeLeave | ||
beforeLeave, | ||
submissions: createSignal(submissions) | ||
}; | ||
} | ||
function createRouteContext(router, parent, child, match, params) { | ||
function createRouteContext(router, parent, outlet, match, params) { | ||
const { | ||
base, | ||
location, | ||
navigatorFactory | ||
location | ||
} = router; | ||
const { | ||
pattern, | ||
element: outlet, | ||
preload, | ||
data | ||
component, | ||
load | ||
} = match().route; | ||
const path = createMemo(() => match().path); | ||
preload && preload(); | ||
const route = { | ||
parent, | ||
pattern, | ||
get child() { | ||
return child(); | ||
}, | ||
path, | ||
params, | ||
data: parent.data, | ||
outlet, | ||
outlet: () => component ? createComponent(component, { | ||
params, | ||
location, | ||
get children() { | ||
return outlet(); | ||
} | ||
}) : outlet(), | ||
resolvePath(to) { | ||
@@ -767,19 +844,11 @@ return resolvePath(base.path(), to, path()); | ||
}; | ||
if (data) { | ||
try { | ||
TempRoute = route; | ||
route.data = data({ | ||
data: parent.data, | ||
params, | ||
location, | ||
navigate: navigatorFactory(route) | ||
}); | ||
} finally { | ||
TempRoute = undefined; | ||
} | ||
} | ||
component && component.preload && component.preload(); | ||
load && load({ | ||
params, | ||
location | ||
}); | ||
return route; | ||
} | ||
const _tmpl$ = /*#__PURE__*/template(`<a link>`); | ||
const _tmpl$ = /*#__PURE__*/template(`<a></a>`, 2); | ||
const Router = props => { | ||
@@ -790,5 +859,3 @@ let e; | ||
url, | ||
base, | ||
data, | ||
out | ||
base | ||
} = props; | ||
@@ -798,16 +865,22 @@ const integration = source || (isServer ? staticIntegration({ | ||
}) : pathIntegration()); | ||
const routerState = createRouterContext(integration, base, data, out); | ||
const routeDefs = children(() => props.root ? { | ||
component: props.root, | ||
children: props.children | ||
} : props.children); | ||
const branches = createMemo(() => createBranches(routeDefs(), props.base || "")); | ||
const routerState = createRouterContext(integration, branches, base); | ||
return createComponent$1(RouterContextObj.Provider, { | ||
value: routerState, | ||
get children() { | ||
return props.children; | ||
return createComponent$1(Routes, { | ||
routerState: routerState, | ||
get branches() { | ||
return branches(); | ||
} | ||
}); | ||
} | ||
}); | ||
}; | ||
const Routes = props => { | ||
const router = useRouter(); | ||
const parentRoute = useRoute(); | ||
const routeDefs = children(() => props.children); | ||
const branches = createMemo(() => createBranches(routeDefs(), joinPaths(parentRoute.pattern, props.base || ""), Outlet)); | ||
const matches = createMemo(() => getRouteMatches(branches(), router.location.pathname)); | ||
function Routes(props) { | ||
const matches = createMemo(() => getRouteMatches(props.branches, props.routerState.location.pathname)); | ||
const params = createMemoObject(() => { | ||
@@ -821,14 +894,2 @@ const m = matches(); | ||
}); | ||
if (router.out) { | ||
router.out.matches.push(matches().map(({ | ||
route, | ||
path, | ||
params | ||
}) => ({ | ||
originalPath: route.originalPath, | ||
pattern: route.pattern, | ||
path, | ||
params | ||
}))); | ||
} | ||
const disposers = []; | ||
@@ -851,3 +912,3 @@ let root; | ||
disposers[i] = dispose; | ||
next[i] = createRouteContext(router, next[i - 1] || parentRoute, () => routeStates()[i + 1], () => matches()[i], params); | ||
next[i] = createRouteContext(props.routerState, next[i - 1] || props.routerState.base, createOutlet(() => routeStates()[i + 1]), () => matches()[i], params); | ||
}); | ||
@@ -875,22 +936,7 @@ } | ||
}); | ||
}; | ||
const useRoutes = (routes, base) => { | ||
return () => createComponent$1(Routes, { | ||
base: base, | ||
children: routes | ||
}); | ||
}; | ||
const Route = props => { | ||
const childRoutes = children(() => props.children); | ||
return mergeProps(props, { | ||
get children() { | ||
return childRoutes(); | ||
} | ||
}); | ||
}; | ||
const Outlet = () => { | ||
const route = useRoute(); | ||
return createComponent$1(Show, { | ||
} | ||
const createOutlet = child => { | ||
return () => createComponent$1(Show, { | ||
get when() { | ||
return route.child; | ||
return child(); | ||
}, | ||
@@ -906,2 +952,10 @@ keyed: true, | ||
}; | ||
const Route = props => { | ||
const childRoutes = children(() => props.children); | ||
return mergeProps(props, { | ||
get children() { | ||
return childRoutes(); | ||
} | ||
}); | ||
}; | ||
function A(props) { | ||
@@ -924,3 +978,3 @@ props = mergeProps({ | ||
return (() => { | ||
const _el$ = _tmpl$(); | ||
const _el$ = _tmpl$.cloneNode(true); | ||
spread(_el$, mergeProps$1(rest, { | ||
@@ -968,2 +1022,208 @@ get href() { | ||
export { A, A as Link, A as NavLink, Navigate, Outlet, Route, Router, Routes, mergeSearchString as _mergeSearchString, createBeforeLeave, createIntegration, createMemoryHistory, hashIntegration, memoryIntegration, normalizeIntegration, pathIntegration, staticIntegration, useBeforeLeave, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useRouteData, useRoutes, useSearchParams }; | ||
/** | ||
* This is mock of the eventual Solid 2.0 primitive. It is not fully featured. | ||
*/ | ||
function createAsync(fn) { | ||
const [resource] = createResource(() => fn(), v => v); | ||
return () => resource(); | ||
} | ||
const LocationHeader = "Location"; | ||
const PRELOAD_TIMEOUT = 5000; | ||
let cacheMap = new Map(); | ||
function getCache() { | ||
if (!isServer) return cacheMap; | ||
const req = getRequestEvent() || sharedConfig.context; | ||
return req.routerCache || (req.routerCache = new Map()); | ||
} | ||
function revalidate(key) { | ||
return startTransition(() => { | ||
const now = Date.now(); | ||
for (let k of cacheMap.keys()) { | ||
if (key === undefined || k === key) { | ||
const set = cacheMap.get(k)[3]; | ||
revalidateSignals(set, now); | ||
cacheMap.delete(k); | ||
} | ||
} | ||
}); | ||
} | ||
function revalidateSignals(set, time) { | ||
for (let s of set) s[1](time); | ||
} | ||
function cache(fn, name, options) { | ||
const [store, setStore] = createStore({}); | ||
return (...args) => { | ||
const cache = getCache(); | ||
const intent = getIntent(); | ||
const owner = getOwner(); | ||
const navigate = owner ? useNavigate() : undefined; | ||
const now = Date.now(); | ||
const key = name + (args.length ? ":" + args.join(":") : ""); | ||
let cached = cache.get(key); | ||
let version; | ||
if (owner) { | ||
version = createSignal(now, { | ||
equals: (p, v) => v - p < 50 // margin of error | ||
}); | ||
onCleanup(() => cached[3].delete(version)); | ||
version[0](); // track it; | ||
} | ||
if (cached && (isServer || intent === "native" || Date.now() - cached[0] < PRELOAD_TIMEOUT)) { | ||
version && cached[3].add(version); | ||
if (cached[2] === "preload" && intent !== "preload") { | ||
cached[0] = now; | ||
cached[1] = "then" in cached[1] ? cached[1].then(handleResponse) : handleResponse(cached[1]); | ||
cached[2] = intent; | ||
} | ||
if (!isServer && intent === "navigate") { | ||
startTransition(() => revalidateSignals(cached[3], cached[0])); // update version | ||
} | ||
return cached[1]; | ||
} | ||
let res = fn(...args); | ||
if (intent !== "preload") { | ||
res = "then" in res ? res.then(handleResponse) : handleResponse(res); | ||
} | ||
if (cached) { | ||
cached[0] = now; | ||
cached[1] = res; | ||
cached[2] = intent; | ||
version && cached[3].add(version); | ||
if (!isServer && intent === "navigate") { | ||
startTransition(() => revalidateSignals(cached[3], cached[0])); // update version | ||
} | ||
} else cache.set(key, cached = [now, res, intent, new Set(version ? [version] : [])]); | ||
return res; | ||
function handleRedirect(response) { | ||
startTransition(() => { | ||
let url = response.headers.get(LocationHeader); | ||
if (url && url.startsWith("/")) { | ||
navigate(url, { | ||
replace: true | ||
}); | ||
} else if (!isServer && url) { | ||
window.location.href = url; | ||
} | ||
}); | ||
} | ||
function handleResponse(v) { | ||
if (v instanceof Response && redirectStatusCodes.has(v.status)) { | ||
if (navigate) isServer ? handleRedirect(v) : setTimeout(() => handleRedirect(v), 0); | ||
return; | ||
} | ||
setStore(key, reconcile(v, options)); | ||
return store[key]; | ||
} | ||
}; | ||
} | ||
function useSubmissions(fn, filter) { | ||
const router = useRouter(); | ||
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.toString() && (!filter || filter(s.input)))); | ||
return new Proxy([], { | ||
get(_, property) { | ||
if (property === $TRACK) return subs(); | ||
if (property === "pending") return subs().some(sub => !sub.result); | ||
return subs()[property]; | ||
} | ||
}); | ||
} | ||
function useSubmission(fn, filter) { | ||
const submissions = useSubmissions(fn, filter); | ||
return { | ||
get clear() { | ||
return submissions[0]?.clear; | ||
}, | ||
get retry() { | ||
return submissions[0]?.retry; | ||
}, | ||
get url() { | ||
return submissions[0]?.url; | ||
}, | ||
get input() { | ||
return submissions[0]?.input; | ||
}, | ||
get result() { | ||
return submissions[0]?.result; | ||
}, | ||
get pending() { | ||
return submissions[0]?.pending; | ||
} | ||
}; | ||
} | ||
function action(fn, name) { | ||
function mutate(variables) { | ||
const p = fn(variables); | ||
const [result, setResult] = createSignal(); | ||
let submission; | ||
const router = this; | ||
router.submissions[1](s => [...s, submission = { | ||
input: variables, | ||
url, | ||
get result() { | ||
return result()?.data; | ||
}, | ||
get pending() { | ||
return !result(); | ||
}, | ||
clear() { | ||
router.submissions[1](v => v.filter(i => i.input !== variables)); | ||
}, | ||
retry() { | ||
setResult(undefined); | ||
const p = fn(variables); | ||
p.then(async data => { | ||
const keys = handleResponse(data, router.navigatorFactory()); | ||
await revalidate(keys); | ||
data ? setResult({ | ||
data | ||
}) : submission.clear(); | ||
return data; | ||
}).catch(error => { | ||
setResult({ | ||
data: error | ||
}); | ||
}); | ||
return p; | ||
} | ||
}]); | ||
p.then(async data => { | ||
const keys = handleResponse(data, router.navigatorFactory()); | ||
await revalidate(keys); | ||
data ? setResult({ | ||
data | ||
}) : submission.clear(); | ||
return data; | ||
}).catch(error => { | ||
setResult({ | ||
data: error | ||
}); | ||
}); | ||
return p; | ||
} | ||
const url = fn.url || `action:${name}` || !isServer ? `action:${fn.name}` : ""; | ||
mutate.toString = () => { | ||
if (!url) throw new Error("Client Actions need explicit names if server rendered"); | ||
return url; | ||
}; | ||
if (!isServer) registerAction(url, mutate); | ||
return mutate; | ||
} | ||
function handleResponse(response, navigate) { | ||
if (response instanceof Response && redirectStatusCodes.has(response.status)) { | ||
const locationUrl = response.headers.get("Location") || "/"; | ||
if (locationUrl.startsWith("http")) { | ||
window.location.href = locationUrl; | ||
} else { | ||
navigate(locationUrl); | ||
} | ||
} | ||
// return keys | ||
return; | ||
} | ||
export { A, A as Link, A as NavLink, Navigate, Route, Router, mergeSearchString as _mergeSearchString, action, cache, createAsync, createBeforeLeave, createIntegration, createMemoryHistory, hashIntegration, memoryIntegration, normalizeIntegration, pathIntegration, revalidate, staticIntegration, useBeforeLeave, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useSubmission, useSubmissions }; |
export * from "./components"; | ||
export * from "./integration"; | ||
export * from "./lifecycle"; | ||
export { useRouteData, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing"; | ||
export { useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing"; | ||
export { mergeSearchString as _mergeSearchString } from "./utils"; | ||
export * from "./data"; |
@@ -1,3 +0,3 @@ | ||
import type { Component, Accessor } from "solid-js"; | ||
import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, MatchFilters, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDataFunc, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types"; | ||
import type { JSX, Accessor } from "solid-js"; | ||
import type { BeforeLeaveEventArgs, Branch, Location, LocationChangeSignal, MatchFilters, NavigateOptions, Navigator, Params, Route, RouteContext, RouteDefinition, RouteMatch, RouterContext, RouterIntegration, SetParams } from "./types"; | ||
export declare const RouterContextObj: import("solid-js").Context<RouterContext | undefined>; | ||
@@ -14,13 +14,12 @@ export declare const RouteContextObj: import("solid-js").Context<RouteContext | undefined>; | ||
export declare const useParams: <T extends Params>() => T; | ||
type MaybeReturnType<T> = T extends (...args: any) => infer R ? R : T; | ||
export declare const useRouteData: <T>() => MaybeReturnType<T>; | ||
export declare const useSearchParams: <T extends Params>() => [T, (params: SetParams, options?: Partial<NavigateOptions>) => void]; | ||
export declare const useBeforeLeave: (listener: (e: BeforeLeaveEventArgs) => void) => void; | ||
export declare function createRoutes(routeDef: RouteDefinition, base?: string, fallback?: Component): Route[]; | ||
export declare function createRoutes(routeDef: RouteDefinition, base?: string): Route[]; | ||
export declare function createBranch(routes: Route[], index?: number): Branch; | ||
export declare function createBranches(routeDef: RouteDefinition | RouteDefinition[], base?: string, fallback?: Component, stack?: Route[], branches?: Branch[]): Branch[]; | ||
export declare function createBranches(routeDef: RouteDefinition | RouteDefinition[], base?: string, stack?: Route[], branches?: Branch[]): Branch[]; | ||
export declare function getRouteMatches(branches: Branch[], location: string): RouteMatch[]; | ||
export declare function createLocation(path: Accessor<string>, state: Accessor<any>): Location; | ||
export declare function createRouterContext(integration?: RouterIntegration | LocationChangeSignal, base?: string, data?: RouteDataFunc, out?: object): RouterContext; | ||
export declare function createRouteContext(router: RouterContext, parent: RouteContext, child: () => RouteContext, match: () => RouteMatch, params: Params): RouteContext; | ||
export {}; | ||
export declare function registerAction(url: string, fn: Function): void; | ||
export declare function getIntent(): "native" | "navigate" | "preload" | undefined; | ||
export declare function createRouterContext(integration?: RouterIntegration | LocationChangeSignal, getBranches?: () => Branch[], base?: string): RouterContext; | ||
export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, params: Params): RouteContext; |
@@ -38,3 +38,2 @@ import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js"; | ||
export const useParams = () => useRoute().params; | ||
export const useRouteData = () => useRoute().data; | ||
export const useSearchParams = () => { | ||
@@ -61,19 +60,9 @@ const location = useLocation(); | ||
}; | ||
export function createRoutes(routeDef, base = "", fallback) { | ||
const { component, data, children } = routeDef; | ||
export function createRoutes(routeDef, base = "") { | ||
const { component, load, children } = routeDef; | ||
const isLeaf = !children || (Array.isArray(children) && !children.length); | ||
const shared = { | ||
key: routeDef, | ||
element: component | ||
? () => createComponent(component, {}) | ||
: () => { | ||
const { element } = routeDef; | ||
return element === undefined && fallback | ||
? createComponent(fallback, {}) | ||
: element; | ||
}, | ||
preload: routeDef.component | ||
? component.preload | ||
: routeDef.preload, | ||
data | ||
component, | ||
load | ||
}; | ||
@@ -118,8 +107,10 @@ return asArray(routeDef.path).reduce((acc, path) => { | ||
} | ||
export function createBranches(routeDef, base = "", fallback, stack = [], branches = []) { | ||
export function createBranches(routeDef, base = "", stack = [], branches = []) { | ||
const routeDefs = asArray(routeDef); | ||
for (let i = 0, len = routeDefs.length; i < len; i++) { | ||
const def = routeDefs[i]; | ||
if (def && typeof def === "object" && def.hasOwnProperty("path")) { | ||
const routes = createRoutes(def, base, fallback); | ||
if (def && typeof def === "object") { | ||
if (!def.hasOwnProperty("path")) | ||
def.path = ""; | ||
const routes = createRoutes(def, base); | ||
for (const route of routes) { | ||
@@ -129,3 +120,3 @@ stack.push(route); | ||
if (def.children && !isEmptyArray) { | ||
createBranches(def.children, route.pattern, fallback, stack, branches); | ||
createBranches(def.children, route.pattern, stack, branches); | ||
} | ||
@@ -189,3 +180,11 @@ else { | ||
} | ||
export function createRouterContext(integration, base = "", data, out) { | ||
const actions = new Map(); | ||
export function registerAction(url, fn) { | ||
actions.set(url, fn); | ||
} | ||
let intent; | ||
export function getIntent() { | ||
return intent; | ||
} | ||
export function createRouterContext(integration, getBranches, base = "") { | ||
const { signal: [source, setSource], utils = {} } = normalizeIntegration(integration); | ||
@@ -195,9 +194,4 @@ const parsePath = utils.parsePath || (p => p); | ||
const beforeLeave = utils.beforeLeave || createBeforeLeave(); | ||
let submissions = []; | ||
const basePath = resolvePath("", base); | ||
const output = isServer && out | ||
? Object.assign(out, { | ||
matches: [], | ||
url: undefined | ||
}) | ||
: undefined; | ||
if (basePath === undefined) { | ||
@@ -232,16 +226,2 @@ throw new Error(`${basePath} is not a valid base path`); | ||
}; | ||
if (data) { | ||
try { | ||
TempRoute = baseRoute; | ||
baseRoute.data = data({ | ||
data: undefined, | ||
params: {}, | ||
location, | ||
navigate: navigatorFactory(baseRoute) | ||
}); | ||
} | ||
finally { | ||
TempRoute = undefined; | ||
} | ||
} | ||
function navigateFromRoute(route, to, options) { | ||
@@ -278,5 +258,2 @@ // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths | ||
if (isServer) { | ||
if (output) { | ||
output.url = resolvedTo; | ||
} | ||
const e = getRequestEvent(); | ||
@@ -289,2 +266,3 @@ e && (e.response = Response.redirect(resolvedTo, 302)); | ||
start(() => { | ||
intent = "navigate"; | ||
setReference(resolvedTo); | ||
@@ -295,2 +273,3 @@ setState(nextState); | ||
if (referrers.length === len) { | ||
intent = undefined; | ||
navigateEnd({ | ||
@@ -330,4 +309,7 @@ value: resolvedTo, | ||
start(() => { | ||
intent = "native"; | ||
setReference(value); | ||
setState(state); | ||
}).then(() => { | ||
intent = undefined; | ||
}); | ||
@@ -338,3 +320,7 @@ } | ||
if (!isServer) { | ||
function handleAnchorClick(evt) { | ||
let preloadTimeout = {}; | ||
function isSvg(el) { | ||
return el.namespaceURI === "http://www.w3.org/2000/svg"; | ||
} | ||
function handleAnchor(evt) { | ||
if (evt.defaultPrevented || | ||
@@ -350,6 +336,8 @@ evt.button !== 0 || | ||
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A"); | ||
if (!a || !a.hasAttribute("link")) | ||
if (!a) | ||
return; | ||
const href = a.href; | ||
if (a.target || (!href && !a.hasAttribute("state"))) | ||
const svg = isSvg(a); | ||
const href = svg ? a.href.baseVal : a.href; | ||
const target = svg ? a.target.baseVal : a.target; | ||
if (target || (!href && !a.hasAttribute("state"))) | ||
return; | ||
@@ -359,6 +347,13 @@ const rel = (a.getAttribute("rel") || "").split(/\s+/); | ||
return; | ||
const url = new URL(href); | ||
const url = svg ? new URL(href, document.baseURI) : new URL(href); | ||
if (url.origin !== window.location.origin || | ||
(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase()))) | ||
return; | ||
return [a, url]; | ||
} | ||
function handleAnchorClick(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) | ||
return; | ||
const [a, url] = res; | ||
const to = parsePath(url.pathname + url.search + url.hash); | ||
@@ -374,10 +369,89 @@ const state = a.getAttribute("state"); | ||
} | ||
// ensure delegated events run first | ||
delegateEvents(["click"]); | ||
function doPreload(a, path) { | ||
const preload = a.getAttribute("preload") !== "false"; | ||
const matches = getRouteMatches(getBranches(), path); | ||
const prevIntent = intent; | ||
intent = "preload"; | ||
for (let match in matches) { | ||
const { route, params } = matches[match]; | ||
route.component && | ||
route.component.preload && | ||
route.component.preload(); | ||
preload && route.load && route.load({ params, location }); | ||
} | ||
intent = prevIntent; | ||
} | ||
function handleAnchorPreload(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) | ||
return; | ||
const [a, url] = res; | ||
if (!preloadTimeout[url.pathname]) | ||
doPreload(a, url.pathname); | ||
} | ||
function handleAnchorIn(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) | ||
return; | ||
const [a, url] = res; | ||
if (preloadTimeout[url.pathname]) | ||
return; | ||
preloadTimeout[url.pathname] = setTimeout(() => { | ||
doPreload(a, url.pathname); | ||
delete preloadTimeout[url.pathname]; | ||
}, 50); | ||
} | ||
function handleAnchorOut(evt) { | ||
const res = handleAnchor(evt); | ||
if (!res) | ||
return; | ||
const [, url] = res; | ||
if (preloadTimeout[url.pathname]) { | ||
clearTimeout(preloadTimeout[url.pathname]); | ||
delete preloadTimeout[url.pathname]; | ||
} | ||
} | ||
function handleFormSubmit(evt) { | ||
let actionRef = evt.submitter && evt.submitter.getAttribute("formaction") || evt.target.action; | ||
if (actionRef && actionRef.startsWith("action:")) { | ||
const data = new FormData(evt.target); | ||
actions.get(actionRef.slice(7))(data); | ||
evt.preventDefault(); | ||
} | ||
} | ||
; | ||
// ensure delegated event run first | ||
delegateEvents(["click", "submit"]); | ||
document.addEventListener("click", handleAnchorClick); | ||
onCleanup(() => document.removeEventListener("click", handleAnchorClick)); | ||
document.addEventListener("mouseover", handleAnchorIn); | ||
document.addEventListener("mouseout", handleAnchorOut); | ||
document.addEventListener("focusin", handleAnchorPreload); | ||
document.addEventListener("touchstart", handleAnchorPreload); | ||
document.addEventListener("submit", handleFormSubmit); | ||
onCleanup(() => { | ||
document.removeEventListener("click", handleAnchorClick); | ||
document.removeEventListener("mouseover", handleAnchorIn); | ||
document.removeEventListener("mouseout", handleAnchorOut); | ||
document.removeEventListener("focusin", handleAnchorPreload); | ||
document.removeEventListener("touchstart", handleAnchorPreload); | ||
document.removeEventListener("submit", handleFormSubmit); | ||
}); | ||
} | ||
else { | ||
function initFromFlash(params) { | ||
let param = params.form ? JSON.parse(params.form) : null; | ||
if (!param || !param.result) { | ||
return []; | ||
} | ||
const input = new Map(param.entries); | ||
return [{ | ||
url: param.url, | ||
result: param.error ? new Error(param.result.message) : param.result, | ||
input: input | ||
}]; | ||
} | ||
submissions = initFromFlash(location.query); | ||
} | ||
return { | ||
base: baseRoute, | ||
out: output, | ||
location, | ||
@@ -388,20 +462,24 @@ isRouting, | ||
navigatorFactory, | ||
beforeLeave | ||
beforeLeave, | ||
submissions: createSignal(submissions) | ||
}; | ||
} | ||
export function createRouteContext(router, parent, child, match, params) { | ||
const { base, location, navigatorFactory } = router; | ||
const { pattern, element: outlet, preload, data } = match().route; | ||
export function createRouteContext(router, parent, outlet, match, params) { | ||
const { base, location } = router; | ||
const { pattern, component, load } = match().route; | ||
const path = createMemo(() => match().path); | ||
preload && preload(); | ||
const route = { | ||
parent, | ||
pattern, | ||
get child() { | ||
return child(); | ||
}, | ||
path, | ||
params, | ||
data: parent.data, | ||
outlet, | ||
outlet: () => component | ||
? createComponent(component, { | ||
params, | ||
location, | ||
get children() { | ||
return outlet(); | ||
} | ||
}) | ||
: outlet(), | ||
resolvePath(to) { | ||
@@ -411,12 +489,7 @@ return resolvePath(base.path(), to, path()); | ||
}; | ||
if (data) { | ||
try { | ||
TempRoute = route; | ||
route.data = data({ data: parent.data, params, location, navigate: navigatorFactory(route) }); | ||
} | ||
finally { | ||
TempRoute = undefined; | ||
} | ||
} | ||
component && | ||
component.preload && | ||
component.preload(); | ||
load && load({ params, location }); | ||
return route; | ||
} |
@@ -1,5 +0,6 @@ | ||
import { Component, JSX } from "solid-js"; | ||
import { Component, JSX, Signal } from "solid-js"; | ||
declare module "solid-js/web" { | ||
interface RequestEvent { | ||
response?: Response; | ||
routerCache?: Map<any, any>; | ||
} | ||
@@ -41,22 +42,17 @@ } | ||
} | ||
export interface RouteDataFuncArgs<T = unknown> { | ||
data: T extends RouteDataFunc<infer _, infer R> ? R : T; | ||
export interface RouteLoadFuncArgs { | ||
params: Params; | ||
location: Location; | ||
navigate: Navigator; | ||
} | ||
export type RouteDataFunc<T = unknown, R = unknown> = (args: RouteDataFuncArgs<T>) => R; | ||
export type RouteLoadFunc = (args: RouteLoadFuncArgs) => void; | ||
export interface RouteSectionProps extends RouteLoadFuncArgs { | ||
children?: JSX.Element; | ||
} | ||
export type RouteDefinition<S extends string | string[] = any> = { | ||
path: S; | ||
matchFilters?: MatchFilters<S>; | ||
data?: RouteDataFunc; | ||
load?: RouteLoadFunc; | ||
children?: RouteDefinition | RouteDefinition[]; | ||
} & ({ | ||
element?: never; | ||
component: Component; | ||
} | { | ||
component?: never; | ||
element?: JSX.Element; | ||
preload?: () => void; | ||
}); | ||
component?: Component<RouteSectionProps>; | ||
}; | ||
export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean); | ||
@@ -84,5 +80,4 @@ export type PathParams<P extends string | readonly string[]> = P extends `${infer Head}/${infer Tail}` ? [...PathParams<Head>, ...PathParams<Tail>] : P extends `:${infer S}?` ? [S] : P extends `:${infer S}` ? [S] : P extends `*${infer S}` ? [S] : []; | ||
pattern: string; | ||
element: () => JSX.Element; | ||
preload?: () => void; | ||
data?: RouteDataFunc; | ||
component?: Component<RouteSectionProps>; | ||
load?: RouteLoadFunc; | ||
matcher: (location: string) => PathMatch | null; | ||
@@ -99,3 +94,2 @@ matchFilters?: MatchFilters; | ||
child?: RouteContext; | ||
data?: unknown; | ||
pattern: string; | ||
@@ -125,3 +119,2 @@ params: Params; | ||
base: RouteContext; | ||
out?: RouterOutput; | ||
location: Location; | ||
@@ -133,2 +126,3 @@ navigatorFactory: NavigatorFactory; | ||
beforeLeave: BeforeLeaveLifecycle; | ||
submissions: Signal<Submission<any, any>[]>; | ||
} | ||
@@ -152,1 +146,9 @@ export interface BeforeLeaveEventArgs { | ||
} | ||
export type Submission<T, U> = { | ||
readonly input: T; | ||
readonly result?: U; | ||
readonly pending: boolean; | ||
readonly url: string; | ||
clear: () => void; | ||
retry: () => void; | ||
}; |
import type { MatchFilters, Params, PathMatch, Route, SetParams } from "./types"; | ||
export declare const redirectStatusCodes: Set<number>; | ||
export declare function normalizePath(path: string, omitSlash?: boolean): string; | ||
@@ -3,0 +4,0 @@ export declare function resolvePath(base: string, path: string, from?: string): string | undefined; |
import { createMemo, getOwner, runWithOwner } from "solid-js"; | ||
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i; | ||
const trimPathRegex = /^\/+|(\/)\/+$/g; | ||
export const redirectStatusCodes = new Set([204, 301, 302, 303, 307, 308]); | ||
export function normalizePath(path, omitSlash = false) { | ||
@@ -5,0 +6,0 @@ const s = path.replace(trimPathRegex, "$1"); |
@@ -9,3 +9,3 @@ { | ||
"license": "MIT", | ||
"version": "0.9.1", | ||
"version": "0.10.0-beta.0", | ||
"homepage": "https://github.com/solidjs/solid-router#readme", | ||
@@ -39,3 +39,3 @@ "repository": { | ||
"@types/jest": "^29.0.0", | ||
"@types/node": "^18.7.14", | ||
"@types/node": "^20.9.0", | ||
"babel-jest": "^29.0.1", | ||
@@ -49,3 +49,3 @@ "babel-preset-solid": "^1.6.6", | ||
"solid-js": "^1.8.4", | ||
"typescript": "^4.9.4" | ||
"typescript": "^5.2.2" | ||
}, | ||
@@ -52,0 +52,0 @@ "peerDependencies": { |
607
README.md
@@ -7,4 +7,2 @@ <p> | ||
#### Note: v0.9.0 requires Solid 1.8.4 or later | ||
A router lets you change your view based on the URL in the browser. This allows your "single-page" application to simulate a traditional multipage site. To use Solid Router, you specify components called Routes that depend on the value of the URL (the "path"), and the router handles the mechanism of swapping them in and out. | ||
@@ -14,3 +12,3 @@ | ||
It supports all of Solid's SSR methods and has Solid's transitions baked in, so use it freely with suspense, resources, and lazy components. Solid Router also allows you to define a data function that loads parallel to the routes ([render-as-you-fetch](https://epicreact.dev/render-as-you-fetch/)). | ||
It supports all of Solid's SSR methods and has Solid's transitions baked in, so use it freely with suspense, resources, and lazy components. Solid Router also allows you to define a load function that loads parallel to the routes ([render-as-you-fetch](https://epicreact.dev/render-as-you-fetch/)). | ||
@@ -20,9 +18,10 @@ - [Getting Started](#getting-started) | ||
- [Configure Your Routes](#configure-your-routes) | ||
- [Create Links to Your Routes](#create-links-to-your-routes) | ||
- [Create Links to Your Routes](#create-links-to-your-routes) | ||
- [Dynamic Routes](#dynamic-routes) | ||
- [Data Functions](#data-functions) | ||
- [Nested Routes](#nested-routes) | ||
- [Hash Mode Router](#hash-mode-router) | ||
- [Memory Mode Router](#memory-mode-router) | ||
- [Data APIs](#data-apis) | ||
- [Config Based Routing](#config-based-routing) | ||
- [Components](#components) | ||
- [Router Primitives](#router-primitives) | ||
@@ -36,3 +35,2 @@ - [useParams](#useparams) | ||
- [useMatch](#usematch) | ||
- [useRoutes](#useroutes) | ||
- [useBeforeLeave](#usebeforeleave) | ||
@@ -49,3 +47,3 @@ - [SPAs in Deployed Environments](#spas-in-deployed-environments) | ||
Install `@solidjs/router`, then wrap your root component with the Router component: | ||
Install `@solidjs/router`, then start your application by rendering the router component | ||
@@ -55,10 +53,5 @@ ```jsx | ||
import { Router } from "@solidjs/router"; | ||
import App from "./App"; | ||
render( | ||
() => ( | ||
<Router> | ||
<App /> | ||
</Router> | ||
), | ||
() => <Router />, | ||
document.getElementById("app") | ||
@@ -68,3 +61,3 @@ ); | ||
This sets up a context so that we can display the routes anywhere in the app. | ||
This sets up a Router that will match on the url to display the desired page | ||
@@ -75,21 +68,25 @@ ### Configure Your Routes | ||
1. Use the `Routes` component to specify where the routes should appear in your app. | ||
1. Add each route to a `<Router>` using the `Route` component, specifying a path and an element or component to render when the user navigates to that path. | ||
```jsx | ||
import { Routes, Route } from "@solidjs/router"; | ||
import { render } from "solid-js/web"; | ||
import { Router, Route } from "@solidjs/router"; | ||
export default function App() { | ||
return ( | ||
<> | ||
<h1>My Site with Lots of Pages</h1> | ||
<Routes></Routes> | ||
</> | ||
); | ||
} | ||
import Home from "./pages/Home"; | ||
import Users from "./pages/Users"; | ||
render(() => ( | ||
<Router> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
</Router> | ||
), document.getElementById("app")); | ||
``` | ||
2. Provide a root level layout | ||
2. Add each route using the `Route` component, specifying a path and an element or component to render when the user navigates to that path. | ||
This will always be there and won't update on page change. It is the ideal place to put top level navigation and Context Providers | ||
```jsx | ||
import { Routes, Route } from "@solidjs/router"; | ||
import { render } from "solid-js/web"; | ||
import { Router, Route } from "@solidjs/router"; | ||
@@ -99,17 +96,15 @@ import Home from "./pages/Home"; | ||
export default function App() { | ||
return ( | ||
<> | ||
<h1>My Site with Lots of Pages</h1> | ||
<Routes> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
<Route | ||
path="/about" | ||
element={<div>This site was made with Solid</div>} | ||
/> | ||
</Routes> | ||
</> | ||
); | ||
} | ||
const App = props => ( | ||
<> | ||
<h1>My Site with lots of pages</h1> | ||
{props.children} | ||
</> | ||
) | ||
render(() => ( | ||
<Router root={App}> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
</Router> | ||
), document.getElementById("app")); | ||
``` | ||
@@ -123,79 +118,52 @@ | ||
import { lazy } from "solid-js"; | ||
import { Routes, Route } from "@solidjs/router"; | ||
import { render } from "solid-js/web"; | ||
import { Router, Route } from "@solidjs/router"; | ||
const Users = lazy(() => import("./pages/Users")); | ||
const Home = lazy(() => import("./pages/Home")); | ||
export default function App() { | ||
return ( | ||
<> | ||
<h1>My Site with Lots of Pages</h1> | ||
<Routes> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
<Route | ||
path="/about" | ||
element={<div>This site was made with Solid</div>} | ||
/> | ||
</Routes> | ||
</> | ||
); | ||
} | ||
const App = props => ( | ||
<> | ||
<h1>My Site with lots of pages</h1> | ||
{props.children} | ||
</> | ||
) | ||
render(() => ( | ||
<Router root={App}> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
</Router> | ||
), document.getElementById("app")); | ||
``` | ||
## Create Links to Your Routes | ||
### Create Links to Your Routes | ||
Use the `A` component to create an anchor tag that takes you to a route: | ||
Use an anchor tag that takes you to a route: | ||
```jsx | ||
import { lazy } from "solid-js"; | ||
import { Routes, Route, A } from "@solidjs/router"; | ||
import { render } from "solid-js/web"; | ||
import { Router, Route } from "@solidjs/router"; | ||
const Users = lazy(() => import("./pages/Users")); | ||
const Home = lazy(() => import("./pages/Home")); | ||
export default function App() { | ||
return ( | ||
<> | ||
<h1>My Site with Lots of Pages</h1> | ||
<nav> | ||
<A href="/about">About</A> | ||
<A href="/">Home</A> | ||
</nav> | ||
<Routes> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
<Route | ||
path="/about" | ||
element={<div>This site was made with Solid</div>} | ||
/> | ||
</Routes> | ||
</> | ||
); | ||
} | ||
``` | ||
const App = props => ( | ||
<> | ||
<nav> | ||
<a href="/about">About</a> | ||
<a href="/">Home</a> | ||
</nav> | ||
<h1>My Site with lots of pages</h1> | ||
{props.children} | ||
</> | ||
); | ||
The `<A>` tag also has an `active` class if its href matches the current location, and `inactive` otherwise. **Note:** By default matching includes locations that are descendents (eg. href `/users` matches locations `/users` and `/users/123`), use the boolean `end` prop to prevent matching these. This is particularly useful for links to the root route `/` which would match everything. | ||
| prop | type | description | | ||
| ------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| href | string | The path of the route to navigate to. This will be resolved relative to the route that the link is in, but you can preface it with `/` to refer back to the root. | | ||
| noScroll | boolean | If true, turn off the default behavior of scrolling to the top of the new page | | ||
| replace | boolean | If true, don't add a new entry to the browser history. (By default, the new page will be added to the browser history, so pressing the back button will take you to the previous route.) | | ||
| state | unknown | [Push this value](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to the history stack when navigating | | ||
| inactiveClass | string | The class to show when the link is inactive (when the current location doesn't match the link) | | ||
| activeClass | string | The class to show when the link is active | | ||
| end | boolean | If `true`, only considers the link to be active when the curent location matches the `href` exactly; if `false`, check if the current location _starts with_ `href` | | ||
### The Navigate Component | ||
Solid Router provides a `Navigate` component that works similarly to `A`, but it will _immediately_ navigate to the provided path as soon as the component is rendered. It also uses the `href` prop, but you have the additional option of passing a function to `href` that returns a path to navigate to: | ||
```jsx | ||
function getPath({ navigate, location }) { | ||
// navigate is the result of calling useNavigate(); location is the result of calling useLocation(). | ||
// You can use those to dynamically determine a path to navigate to | ||
return "/some-path"; | ||
} | ||
// Navigating to /redirect will redirect you to the result of getPath | ||
<Route path="/redirect" element={<Navigate href={getPath} />} />; | ||
render(() => ( | ||
<Router root={App}> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/" component={Home} /> | ||
</Router> | ||
), document.getElementById("app")); | ||
``` | ||
@@ -209,3 +177,5 @@ | ||
import { lazy } from "solid-js"; | ||
import { Routes, Route } from "@solidjs/router"; | ||
import { render } from "solid-js/web"; | ||
import { Router, Route } from "@solidjs/router"; | ||
const Users = lazy(() => import("./pages/Users")); | ||
@@ -215,18 +185,9 @@ const User = lazy(() => import("./pages/User")); | ||
export default function App() { | ||
return ( | ||
<> | ||
<h1>My Site with Lots of Pages</h1> | ||
<Routes> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/users/:id" component={User} /> | ||
<Route path="/" component={Home} /> | ||
<Route | ||
path="/about" | ||
element={<div>This site was made with Solid</div>} | ||
/> | ||
</Routes> | ||
</> | ||
); | ||
} | ||
render(() => ( | ||
<Router> | ||
<Route path="/users" component={Users} /> | ||
<Route path="/users/:id" component={User} /> | ||
<Route path="/" component={Home} /> | ||
</Router> | ||
), document.getElementById("app")); | ||
``` | ||
@@ -238,8 +199,7 @@ | ||
> **Note on Animation/Transitions**: | ||
> Routes that share the same path match will be treated as the same route. If you want to force re-render you can wrap your component in a keyed `<Show>` like: | ||
> ```js | ||
><Show when={params.something} keyed><MyComponent></Show> | ||
>``` | ||
**Note on Animation/Transitions**: | ||
Routes that share the same path match will be treated as the same route. If you want to force re-render you can wrap your component in a keyed `<Show>` like: | ||
```jsx | ||
<Show when={params.something} keyed><MyComponent></Show> | ||
``` | ||
--- | ||
@@ -250,10 +210,9 @@ | ||
```tsx | ||
```jsx | ||
import { lazy } from "solid-js"; | ||
import { Routes, Route } from "@solidjs/router"; | ||
import { render } from "solid-js/web"; | ||
import { Router, Route } from "@solidjs/router"; | ||
import type { SegmentValidators } from "./types"; | ||
const Users = lazy(() => import("./pages/Users")); | ||
const User = lazy(() => import("./pages/User")); | ||
const Home = lazy(() => import("./pages/Home")); | ||
@@ -266,16 +225,11 @@ const filters: MatchFilters = { | ||
export default function App() { | ||
return ( | ||
<> | ||
<h1>My Site with Lots of Pages</h1> | ||
<Routes> | ||
<Route | ||
path="/users/:parent/:id/:withHtmlExtension" | ||
component={User} | ||
matchFilters={filters} | ||
/> | ||
</Routes> | ||
</> | ||
); | ||
} | ||
render(() => ( | ||
<Router> | ||
<Route | ||
path="/users/:parent/:id/:withHtmlExtension" | ||
component={User} | ||
matchFilters={filters} | ||
/> | ||
</Router> | ||
), document.getElementById("app")); | ||
``` | ||
@@ -296,15 +250,2 @@ | ||
```jsx | ||
// async fetching function | ||
import { fetchUser } ... | ||
export default function User () { | ||
const params = useParams(); | ||
const [userData] = createResource(() => params.id, fetchUser); | ||
return <A href={userData.twitter}>{userData.name}</A> | ||
} | ||
``` | ||
### Optional Parameters | ||
@@ -316,3 +257,3 @@ | ||
// Matches stories and stories/123 but not stories/123/comments | ||
<Route path="/stories/:id?" element={<Stories />} /> | ||
<Route path="/stories/:id?" component={Stories} /> | ||
``` | ||
@@ -332,3 +273,3 @@ | ||
```jsx | ||
<Route path="foo/*any" element={<div>{useParams().any}</div>} /> | ||
<Route path="foo/*any" component={Foo} /> | ||
``` | ||
@@ -347,59 +288,2 @@ | ||
## Data Functions | ||
In the [above example](#dynamic-routes), the User component is lazy-loaded and then the data is fetched. With route data functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. | ||
To do this, create a function that fetches and returns the data using `createResource`. Then pass that function to the `data` prop of the `Route` component. | ||
```js | ||
import { lazy } from "solid-js"; | ||
import { Route } from "@solidjs/router"; | ||
import { fetchUser } ... | ||
const User = lazy(() => import("./pages/users/[id].js")); | ||
// Data function | ||
function UserData({params, location, navigate, data}) { | ||
const [user] = createResource(() => params.id, fetchUser); | ||
return user; | ||
} | ||
// Pass it in the route definition | ||
<Route path="/users/:id" component={User} data={UserData} />; | ||
``` | ||
When the route is loaded, the data function is called, and the result can be accessed by calling `useRouteData()` in the route component. | ||
```jsx | ||
// pages/users/[id].js | ||
import { useRouteData } from "@solidjs/router"; | ||
export default function User() { | ||
const user = useRouteData(); | ||
return <h1>{user().name}</h1>; | ||
} | ||
``` | ||
As its only argument, the data function is passed an object that you can use to access route information: | ||
| key | type | description | | ||
| -------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | ||
| params | object | The route parameters (same value as calling `useParams()` inside the route component) | | ||
| location | `{ pathname, search, hash, query, state, key}` | An object that you can use to get more information about the path (corresponds to [`useLocation()`](#uselocation)) | | ||
| navigate | `(to: string, options?: NavigateOptions) => void` | A function that you can call to navigate to a different route instead (corresponds to [`useNavigate()`](#usenavigate)) | | ||
| data | unknown | The data returned by the [parent's](#nested-routes) data function, if any. (Data will pass through any intermediate nesting.) | | ||
A common pattern is to export the data function that corresponds to a route in a dedicated `route.data.js` file. This way, the data function can be imported without loading anything else. | ||
```js | ||
import { lazy } from "solid-js"; | ||
import { Route } from "@solidjs/router"; | ||
import { fetchUser } ... | ||
import UserData from "./pages/users/[id].data.js"; | ||
const User = lazy(() => import("/pages/users/[id].js")); | ||
// In the Route definition | ||
<Route path="/users/:id" component={User} data={UserData} />; | ||
``` | ||
## Nested Routes | ||
@@ -440,12 +324,10 @@ | ||
You can also take advantage of nesting by adding a parent element with an `<Outlet/>`. | ||
You can also take advantage of nesting by using `props.children` passed to the route component. | ||
```jsx | ||
import { Outlet } from "@solidjs/router"; | ||
function PageWrapper() { | ||
function PageWrapper(props) { | ||
return ( | ||
<div> | ||
<h1> We love our users! </h1> | ||
<Outlet /> | ||
{props.children} | ||
<A href="/">Back Home</A> | ||
@@ -462,3 +344,3 @@ </div> | ||
The routes are still configured the same, but now the route elements will appear inside the parent element where the `<Outlet/>` was declared. | ||
The routes are still configured the same, but now the route elements will appear inside the parent element where the `props.children` was declared. | ||
@@ -470,5 +352,5 @@ You can nest indefinitely - just remember that only leaf nodes will become their own routes. In this example, the only route created is `/layer1/layer2`, and it appears as three nested divs. | ||
path="/" | ||
element={ | ||
component={(props) => | ||
<div> | ||
Onion starts here <Outlet /> | ||
Onion starts here {props.children} | ||
</div> | ||
@@ -479,9 +361,10 @@ } | ||
path="layer1" | ||
element={ | ||
component={(props) => | ||
<div> | ||
Another layer <Outlet /> | ||
Another layer {props.children} | ||
</div> | ||
} | ||
> | ||
<Route path="layer2" element={<div>Innermost layer</div>}></Route> | ||
<Route path="layer2" | ||
component={() => <div>Innermost layer</div>}> </Route> | ||
</Route> | ||
@@ -491,31 +374,146 @@ </Route> | ||
If you declare a `data` function on a parent and a child, the result of the parent's data function will be passed to the child's data function as the `data` property of the argument, as described in the last section. This works even if it isn't a direct child, because by default every route forwards its parent's data. | ||
## Data APIs | ||
## Hash Mode Router | ||
### `cache` | ||
By default, Solid Router uses `location.pathname` as route path. You can simply switch to hash mode through the `source` property on `<Router>` component. | ||
To prevent duplicate fetching and to trigger handle refetching we provide a cache api. That takes a function and returns the same function. | ||
```jsx | ||
import { Router, hashIntegration } from "@solidjs/router"; | ||
const getUser = cache((id) => { | ||
return (await fetch(`/api/users${id}`)).json() | ||
}, "users") // used as cache key + serialized arguments | ||
``` | ||
It is expected that the arguments to the cache function are serializable. | ||
<Router source={hashIntegration()}> | ||
<App /> | ||
</Router>; | ||
This cache accomplishes the following: | ||
1. It does just deduping on the server for the lifetime of the request. | ||
2. It does preload cache in the browser which lasts 10 seconds. When a route is preloaded on hover or when load is called when entering a route it will make sure to dedupe calls. | ||
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation. | ||
4. It will serve as a back/forward cache for browser navigation up to 5 mins. Any user based navigation or link click bypasses it. Revalidation or new fetch updates the cache. | ||
This cache can be defined anywhere and then used inside your components with: | ||
### `createAsync` | ||
This is light wrapper over `createResource` that aims to serve as stand-in for a future primitive we intend to bring to Solid core in 2.0. It is a simpler async primitive where the function tracks like `createMemo` and it expects a promise back that it turns into a Signal. Reading it before it ready causes Suspense/Transitions to trigger. | ||
```jsx | ||
const user = createAsync(() => getUser(params.id)) | ||
``` | ||
## Memory Mode Router | ||
`createAsync` is designed to only work with cached functions otherwise it will over fetch. If not using `cache`, continue using `createResource` instead. | ||
You can also use memory mode router for testing purpose. | ||
### `action` | ||
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response builders can be found below(TODO). | ||
```jsx | ||
import { Router, memoryIntegration } from "@solidjs/router"; | ||
// anywhere | ||
const myAction = action(async (data) => { | ||
await doMutation(data); | ||
return redirect("/", { | ||
invalidate: [getUser, data.id] | ||
}) // returns a response | ||
}); | ||
<Router source={memoryIntegration()}> | ||
<App /> | ||
</Router>; | ||
// in component | ||
<form action={myAction} /> | ||
//or | ||
<button type="submit" formaction={myAction}></button> | ||
``` | ||
#### Notes of `<form>` implementation and SSR | ||
This requires stable references as you can only serialize a string as an attribute, and across SSR they'd need to match. The solution is providing a unique name. | ||
```jsx | ||
const myAction = action(async (args) => {}, "my-action"); | ||
``` | ||
### `useAction` | ||
Instead of forms you can use actions directly by wrapping them in a `useAction` primitive. This is how we get the router context. | ||
```jsx | ||
// in component | ||
const submit = useAction(myAction) | ||
submit(...args) | ||
``` | ||
The outside of a form context you can use custom data instead of formData, and these helpers preserve types. | ||
### `useSubmission`/`useSubmissions` | ||
Are used to injecting the optimistic updates while actions are in flight. They either return a single Submission(latest) or all that match with an optional filter function. | ||
```jsx | ||
type Submission<T, U> = { | ||
input: T; | ||
result: U; | ||
error: any; | ||
pending: boolean | ||
clear: () => {} | ||
retry: () => {} | ||
} | ||
const submissions = useSubmissions(action, (input) => filter(input)); | ||
const submission = useSubmission(action, (input) => filter(input)); | ||
``` | ||
### Load Functions | ||
Even with the cache API it is possible that we have waterfalls both with view logic and with lazy loaded code. With load functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. | ||
To do this, we can call our cache function in the load function. | ||
```js | ||
import { lazy } from "solid-js"; | ||
import { Route } from "@solidjs/router"; | ||
import { getUser } from ... // the cache function | ||
const User = lazy(() => import("./pages/users/[id].js")); | ||
// load function | ||
function loadUser({params, location}) { | ||
void getUser(params.id) | ||
} | ||
// Pass it in the route definition | ||
<Route path="/users/:id" component={User} load={loadUser} />; | ||
``` | ||
The load function is called when the Route is loaded or eagerly when links are hovered. Inside your page component you | ||
```jsx | ||
// pages/users/[id].js | ||
import { getUser } from ... // the cache function | ||
export default function User(props) { | ||
const user = createAsync(() => getUser(props.params.id)); | ||
return <h1>{user().name}</h1>; | ||
} | ||
``` | ||
As its only argument, the load function is passed an object that you can use to access route information: | ||
| key | type | description | | ||
| -------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | ||
| params | object | The route parameters (same value as calling `useParams()` inside the route component) | | ||
| location | `{ pathname, search, hash, query, state, key}` | An object that you can use to get more information about the path (corresponds to [`useLocation()`](#uselocation)) | | ||
A common pattern is to export the preload function and data wrappers that corresponds to a route in a dedicated `route.data.js` file. This way, the data function can be imported without loading anything else. | ||
```js | ||
import { lazy } from "solid-js"; | ||
import { Route } from "@solidjs/router"; | ||
import loadUser from "./pages/users/[id].data.js"; | ||
const User = lazy(() => import("/pages/users/[id].js")); | ||
// In the Route definition | ||
<Route path="/users/:id" component={User} load={loadUser} />; | ||
``` | ||
## Config Based Routing | ||
You don't have to use JSX to set up your routes; you can pass an object directly with `useRoutes`: | ||
You don't have to use JSX to set up your routes; you can pass an object: | ||
@@ -525,3 +523,3 @@ ```jsx | ||
import { render } from "solid-js/web"; | ||
import { Router, useRoutes, A } from "@solidjs/router"; | ||
import { Router } from "@solidjs/router"; | ||
@@ -561,24 +559,4 @@ const routes = [ | ||
function App() { | ||
const Routes = useRoutes(routes); | ||
return ( | ||
<> | ||
<h1>Awesome Site</h1> | ||
<A class="nav" href="/"> | ||
Home | ||
</A> | ||
<A class="nav" href="/users"> | ||
Users | ||
</A> | ||
<Routes /> | ||
</> | ||
); | ||
} | ||
render( | ||
() => ( | ||
<Router> | ||
<App /> | ||
</Router> | ||
), | ||
render(() => | ||
<Router>{routes}</Router>, | ||
document.getElementById("app") | ||
@@ -588,2 +566,65 @@ ); | ||
## Alternative Routers | ||
### Hash Mode Router | ||
By default, Solid Router uses `location.pathname` as route path. You can simply switch to hash mode through the `source` property on `<Router>` component. | ||
```jsx | ||
import { Router, hashIntegration } from "@solidjs/router"; | ||
<Router source={hashIntegration()} />; | ||
``` | ||
### Memory Mode Router | ||
You can also use memory mode router for testing purpose. | ||
```jsx | ||
import { Router, memoryIntegration } from "@solidjs/router"; | ||
<Router source={memoryIntegration()} />; | ||
``` | ||
## Components | ||
### `<A>` | ||
Like the `<a>` tag but supports relative paths and active class styling. | ||
The `<A>` tag has an `active` class if its href matches the current location, and `inactive` otherwise. **Note:** By default matching includes locations that are descendents (eg. href `/users` matches locations `/users` and `/users/123`), use the boolean `end` prop to prevent matching these. This is particularly useful for links to the root route `/` which would match everything. | ||
| prop | type | description | | ||
| ------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| href | string | The path of the route to navigate to. This will be resolved relative to the route that the link is in, but you can preface it with `/` to refer back to the root. | | ||
| noScroll | boolean | If true, turn off the default behavior of scrolling to the top of the new page | | ||
| replace | boolean | If true, don't add a new entry to the browser history. (By default, the new page will be added to the browser history, so pressing the back button will take you to the previous route.) | | ||
| state | unknown | [Push this value](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) to the history stack when navigating | | ||
| inactiveClass | string | The class to show when the link is inactive (when the current location doesn't match the link) | | ||
| activeClass | string | The class to show when the link is active | | ||
| end | boolean | If `true`, only considers the link to be active when the curent location matches the `href` exactly; if `false`, check if the current location _starts with_ `href` | | ||
### `<Navigate />` | ||
Solid Router provides a `Navigate` component that works similarly to `A`, but it will _immediately_ navigate to the provided path as soon as the component is rendered. It also uses the `href` prop, but you have the additional option of passing a function to `href` that returns a path to navigate to: | ||
```jsx | ||
function getPath({ navigate, location }) { | ||
// navigate is the result of calling useNavigate(); location is the result of calling useLocation(). | ||
// You can use those to dynamically determine a path to navigate to | ||
return "/some-path"; | ||
} | ||
// Navigating to /redirect will redirect you to the result of getPath | ||
<Route path="/redirect" component={() => <Navigate href={getPath} />} />; | ||
``` | ||
### `<Route>` | ||
The Component for defining Routes: | ||
| prop | type | description | | ||
|-|-|-| | ||
|TODO | ||
## Router Primitives | ||
@@ -670,14 +711,2 @@ | ||
### useRouteData | ||
Retrieves the return value from the data function. | ||
> In previous versions you could use numbers to access parent data. This is no longer supported. Instead the data functions themselves receive the parent data that you can expose through the specific nested routes data. | ||
```js | ||
const user = useRouteData(); | ||
return <h1>{user().name}</h1>; | ||
``` | ||
### useMatch | ||
@@ -693,6 +722,2 @@ | ||
### useRoutes | ||
Used to define routes via a config object instead of JSX. See [Config Based Routing](#config-based-routing). | ||
### useBeforeLeave | ||
@@ -726,2 +751,20 @@ | ||
## Migrations from 0.8.x | ||
v0.9.0 brings some big changes to support the future of routing including Islands/Partial Hydration hybrid solutions. Most notably there is no Context API available in non-hydrating parts of the application. | ||
The biggest changes are around removed APIs that need to be replaced. | ||
### `<Outlet>`, `<Routes>`, `useRoutes` | ||
This is no longer used and instead will use `props.children` passed from into the page components for outlets. Nested Routes inherently cause waterfalls and are Outlets in a sense themselves. We do not want to encourage the pattern and if you must do it you can always nest `<Routers>` with appropriate base path. | ||
## `element` prop removed from `Route` | ||
Related without Outlet component it has to be passed in manually. At which point the `element` prop has less value. Removing the second way to define route components to reduce confusion and edge cases. | ||
### `data` functions & `useRouteData` | ||
These have been replaced by a load mechanism. This allows link hover preloads (as the load function can be run as much as wanted without worry about reactivity). It support deduping/cache APIs which give more control over how things are cached. It also addresses TS issues with getting the right types in the Component without `typeof` checks. | ||
## SPAs in Deployed Environments | ||
@@ -728,0 +771,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
120650
26
2646
762