@solidjs/router 0.13.5 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -25
- package/dist/data/action.d.ts +5 -5
- package/dist/data/action.js +2 -0
- package/dist/data/cache.d.ts +2 -2
- package/dist/data/cache.js +17 -19
- package/dist/data/events.js +6 -6
- package/dist/data/response.d.ts +4 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +99 -94
- package/dist/index.jsx +1 -1
- package/dist/routers/Router.js +2 -2
- package/dist/routers/components.d.ts +7 -3
- package/dist/routers/components.jsx +8 -8
- package/dist/routing.d.ts +5 -2
- package/dist/routing.js +62 -68
- package/dist/types.d.ts +30 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A router lets you change your view based on the URL in the browser. This allows
|
|
|
10
10
|
|
|
11
11
|
Solid Router is a universal router for SolidJS - it works whether you're rendering on the client or on the server. It was inspired by and combines paradigms of React Router and the Ember Router. Routes can be defined directly in your app's template using JSX, but you can also pass your route configuration directly as an object. It also supports nested routing, so navigation can change a part of a component, rather than completely replacing it.
|
|
12
12
|
|
|
13
|
-
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
|
|
13
|
+
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 preload function that loads parallel to the routes ([render-as-you-fetch](https://epicreact.dev/render-as-you-fetch/)).
|
|
14
14
|
|
|
15
15
|
- [Getting Started](#getting-started)
|
|
16
16
|
- [Set Up the Router](#set-up-the-router)
|
|
@@ -372,17 +372,19 @@ You can nest indefinitely - just remember that only leaf nodes will become their
|
|
|
372
372
|
</div>
|
|
373
373
|
}
|
|
374
374
|
>
|
|
375
|
-
<Route
|
|
376
|
-
|
|
375
|
+
<Route
|
|
376
|
+
path="layer2"
|
|
377
|
+
component={() => <div>Innermost layer</div>}
|
|
378
|
+
/>
|
|
377
379
|
</Route>
|
|
378
380
|
</Route>
|
|
379
381
|
```
|
|
380
382
|
|
|
381
|
-
##
|
|
383
|
+
## Preload Functions
|
|
382
384
|
|
|
383
|
-
Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With
|
|
385
|
+
Even with smart caches it is possible that we have waterfalls both with view logic and with lazy loaded code. With preload functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible. The preload function is called when the Route is loaded or eagerly when links are hovered.
|
|
384
386
|
|
|
385
|
-
As its only argument, the
|
|
387
|
+
As its only argument, the preload function is passed an object that you can use to access route information:
|
|
386
388
|
|
|
387
389
|
```js
|
|
388
390
|
import { lazy } from "solid-js";
|
|
@@ -390,13 +392,13 @@ import { Route } from "@solidjs/router";
|
|
|
390
392
|
|
|
391
393
|
const User = lazy(() => import("./pages/users/[id].js"));
|
|
392
394
|
|
|
393
|
-
//
|
|
394
|
-
function
|
|
395
|
-
// do
|
|
395
|
+
// preload function
|
|
396
|
+
function preloadUser({params, location}) {
|
|
397
|
+
// do preloading
|
|
396
398
|
}
|
|
397
399
|
|
|
398
400
|
// Pass it in the route definition
|
|
399
|
-
<Route path="/users/:id" component={User}
|
|
401
|
+
<Route path="/users/:id" component={User} preload={preloadUser} />;
|
|
400
402
|
```
|
|
401
403
|
|
|
402
404
|
| key | type | description |
|
|
@@ -406,24 +408,24 @@ function loadUser({params, location}) {
|
|
|
406
408
|
| intent | `"initial", "navigate", "native", "preload"` | Indicates why this function is being called. <ul><li>"initial" - the route is being initially shown (ie page load)</li><li>"native" - navigate originated from the browser (eg back/forward)</li><li>"navigate" - navigate originated from the router (eg call to navigate or anchor clicked)</li><li>"preload" - not navigating, just preloading (eg link hover)</li></ul> |
|
|
407
409
|
|
|
408
410
|
|
|
409
|
-
A common pattern is to export the
|
|
411
|
+
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.
|
|
410
412
|
|
|
411
413
|
```js
|
|
412
414
|
import { lazy } from "solid-js";
|
|
413
415
|
import { Route } from "@solidjs/router";
|
|
414
|
-
import
|
|
416
|
+
import preloadUser from "./pages/users/[id].data.js";
|
|
415
417
|
const User = lazy(() => import("/pages/users/[id].js"));
|
|
416
418
|
|
|
417
419
|
// In the Route definition
|
|
418
|
-
<Route path="/users/:id" component={User}
|
|
420
|
+
<Route path="/users/:id" component={User} preload={preloadUser} />;
|
|
419
421
|
```
|
|
420
422
|
|
|
421
|
-
The return value of the `
|
|
423
|
+
The return value of the `preload` function is passed to the page component when called at anytime other than `"preload"` intent, so you can initialize things in there, or alternatively use our new Data APIs:
|
|
422
424
|
|
|
423
425
|
|
|
424
426
|
## Data APIs
|
|
425
427
|
|
|
426
|
-
Keep in mind these are completely optional. To use but showcase the power of our
|
|
428
|
+
Keep in mind these are completely optional. To use but showcase the power of our preload mechanism.
|
|
427
429
|
|
|
428
430
|
### `cache`
|
|
429
431
|
|
|
@@ -439,11 +441,11 @@ It is expected that the arguments to the cache function are serializable.
|
|
|
439
441
|
This cache accomplishes the following:
|
|
440
442
|
|
|
441
443
|
1. It does just deduping on the server for the lifetime of the request.
|
|
442
|
-
2. It does preload cache in the browser which lasts
|
|
444
|
+
2. It does preload cache in the browser which lasts 5 seconds. When a route is preloaded on hover or when preload is called when entering a route it will make sure to dedupe calls.
|
|
443
445
|
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
|
|
444
446
|
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.
|
|
445
447
|
|
|
446
|
-
Using it with
|
|
448
|
+
Using it with preload function might look like:
|
|
447
449
|
|
|
448
450
|
```js
|
|
449
451
|
import { lazy } from "solid-js";
|
|
@@ -452,13 +454,13 @@ import { getUser } from ... // the cache function
|
|
|
452
454
|
|
|
453
455
|
const User = lazy(() => import("./pages/users/[id].js"));
|
|
454
456
|
|
|
455
|
-
//
|
|
456
|
-
function
|
|
457
|
+
// preload function
|
|
458
|
+
function preloadUser({params, location}) {
|
|
457
459
|
void getUser(params.id)
|
|
458
460
|
}
|
|
459
461
|
|
|
460
462
|
// Pass it in the route definition
|
|
461
|
-
<Route path="/users/:id" component={User}
|
|
463
|
+
<Route path="/users/:id" component={User} preload={preloadUser} />;
|
|
462
464
|
```
|
|
463
465
|
|
|
464
466
|
Inside your page component you:
|
|
@@ -768,7 +770,7 @@ The Component for defining Routes:
|
|
|
768
770
|
| component | `Component` | Component that will be rendered for the matched segment |
|
|
769
771
|
| matchFilters | `MatchFilters` | Additional constraints for matching against the route |
|
|
770
772
|
| children | `JSX.Element` | Nested `<Route>` definitions |
|
|
771
|
-
|
|
|
773
|
+
| preload | `RoutePreloadFunc` | Function called during preload or when the route is navigated to. |
|
|
772
774
|
|
|
773
775
|
## Router Primitives
|
|
774
776
|
|
|
@@ -871,6 +873,16 @@ const matches = useCurrentMatches();
|
|
|
871
873
|
const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
872
874
|
```
|
|
873
875
|
|
|
876
|
+
### usePreloadRoute
|
|
877
|
+
|
|
878
|
+
`usePreloadRoute` returns a function that can be used to preload a route manual. This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
|
|
879
|
+
|
|
880
|
+
```js
|
|
881
|
+
const preload = usePreloadRoute();
|
|
882
|
+
|
|
883
|
+
preload(`/users/settings`, { preloadData: true });
|
|
884
|
+
```
|
|
885
|
+
|
|
874
886
|
### useBeforeLeave
|
|
875
887
|
|
|
876
888
|
`useBeforeLeave` takes a function that will be called prior to leaving a route. The function will be called with:
|
|
@@ -917,7 +929,7 @@ Related without Outlet component it has to be passed in manually. At which point
|
|
|
917
929
|
|
|
918
930
|
### `data` functions & `useRouteData`
|
|
919
931
|
|
|
920
|
-
These have been replaced by a
|
|
932
|
+
These have been replaced by a preload mechanism. This allows link hover preloads (as the preload 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.
|
|
921
933
|
|
|
922
934
|
That being said you can reproduce the old pattern largely by turning off preloads at the router level and then injecting your own Context:
|
|
923
935
|
|
|
@@ -927,15 +939,15 @@ import { Route } from "@solidjs/router";
|
|
|
927
939
|
|
|
928
940
|
const User = lazy(() => import("./pages/users/[id].js"));
|
|
929
941
|
|
|
930
|
-
//
|
|
931
|
-
function
|
|
942
|
+
// preload function
|
|
943
|
+
function preloadUser({params, location}) {
|
|
932
944
|
const [user] = createResource(() => params.id, fetchUser);
|
|
933
945
|
return user;
|
|
934
946
|
}
|
|
935
947
|
|
|
936
948
|
// Pass it in the route definition
|
|
937
949
|
<Router preload={false}>
|
|
938
|
-
<Route path="/users/:id" component={User}
|
|
950
|
+
<Route path="/users/:id" component={User} preload={preloadUser} />
|
|
939
951
|
</Router>
|
|
940
952
|
```
|
|
941
953
|
|
package/dist/data/action.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { JSX } from "solid-js";
|
|
2
|
-
import { Submission } from "../types.js";
|
|
3
|
-
export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<U
|
|
2
|
+
import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
|
|
3
|
+
export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
|
|
4
4
|
url: string;
|
|
5
|
-
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<U
|
|
5
|
+
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U>;
|
|
6
6
|
};
|
|
7
7
|
export declare const actions: Map<string, Action<any, any>>;
|
|
8
8
|
export declare function useSubmissions<T extends Array<any>, U>(fn: Action<T, U>, filter?: (arg: T) => boolean): Submission<T, U>[] & {
|
|
9
9
|
pending: boolean;
|
|
10
10
|
};
|
|
11
|
-
export declare function useSubmission<T extends Array<any>, U>(fn: Action<T, U>, filter?: (arg: T) => boolean): Submission<T, U
|
|
12
|
-
export declare function useAction<T extends Array<any>, U>(action: Action<T, U>): (...args: Parameters<Action<T, U>>) => Promise<U
|
|
11
|
+
export declare function useSubmission<T extends Array<any>, U>(fn: Action<T, U>, filter?: (arg: T) => boolean): Submission<T, U> | SubmissionStub;
|
|
12
|
+
export declare function useAction<T extends Array<any>, U>(action: Action<T, U>): (...args: Parameters<Action<T, U>>) => Promise<NarrowResponse<U>>;
|
|
13
13
|
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
|
package/dist/data/action.js
CHANGED
|
@@ -21,6 +21,8 @@ export function useSubmission(fn, filter) {
|
|
|
21
21
|
const submissions = useSubmissions(fn, filter);
|
|
22
22
|
return new Proxy({}, {
|
|
23
23
|
get(_, property) {
|
|
24
|
+
if (submissions.length === 0 && property === "clear" || property === "retry")
|
|
25
|
+
return (() => { });
|
|
24
26
|
return submissions[submissions.length - 1]?.[property];
|
|
25
27
|
}
|
|
26
28
|
});
|
package/dist/data/cache.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { CacheEntry } from "../types.js";
|
|
1
|
+
import type { CacheEntry, NarrowResponse } from "../types.js";
|
|
2
2
|
export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
|
|
3
3
|
export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
|
|
4
4
|
export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
|
|
5
5
|
[K in keyof A]-?: A[K];
|
|
6
|
-
} ? (...args: never[]) => R :
|
|
6
|
+
} ? (...args: never[]) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R> : (...args: A) => R extends Promise<infer P> ? Promise<NarrowResponse<P>> : NarrowResponse<R>) & {
|
|
7
7
|
keyFor: (...args: A) => string;
|
|
8
8
|
key: string;
|
|
9
9
|
} : never;
|
package/dist/data/cache.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
|
|
2
2
|
import { getRequestEvent, isServer } from "solid-js/web";
|
|
3
|
-
import { useNavigate, getIntent,
|
|
3
|
+
import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
|
|
4
4
|
const LocationHeader = "Location";
|
|
5
5
|
const PRELOAD_TIMEOUT = 5000;
|
|
6
6
|
const CACHE_TIMEOUT = 180000;
|
|
@@ -47,7 +47,7 @@ export function cache(fn, name) {
|
|
|
47
47
|
const cachedFn = ((...args) => {
|
|
48
48
|
const cache = getCache();
|
|
49
49
|
const intent = getIntent();
|
|
50
|
-
const
|
|
50
|
+
const inPreloadFn = getInPreloadFn();
|
|
51
51
|
const owner = getOwner();
|
|
52
52
|
const navigate = owner ? useNavigate() : undefined;
|
|
53
53
|
const now = Date.now();
|
|
@@ -87,15 +87,14 @@ export function cache(fn, name) {
|
|
|
87
87
|
cached[0] = now;
|
|
88
88
|
}
|
|
89
89
|
let res = cached[1];
|
|
90
|
-
if (
|
|
90
|
+
if (intent !== "preload") {
|
|
91
91
|
res =
|
|
92
92
|
"then" in cached[1]
|
|
93
93
|
? cached[1].then(handleResponse(false), handleResponse(true))
|
|
94
94
|
: handleResponse(false)(cached[1]);
|
|
95
95
|
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
|
|
96
96
|
}
|
|
97
|
-
|
|
98
|
-
"then" in res && res.catch(() => { });
|
|
97
|
+
inPreloadFn && "then" in res && res.catch(() => { });
|
|
99
98
|
return res;
|
|
100
99
|
}
|
|
101
100
|
let res = !isServer && sharedConfig.context && sharedConfig.has(key)
|
|
@@ -120,14 +119,13 @@ export function cache(fn, name) {
|
|
|
120
119
|
if (e && e.router.dataOnly)
|
|
121
120
|
return (e.router.data[key] = res);
|
|
122
121
|
}
|
|
123
|
-
if (
|
|
122
|
+
if (intent !== "preload") {
|
|
124
123
|
res =
|
|
125
124
|
"then" in res
|
|
126
125
|
? res.then(handleResponse(false), handleResponse(true))
|
|
127
126
|
: handleResponse(false)(res);
|
|
128
127
|
}
|
|
129
|
-
|
|
130
|
-
"then" in res && res.catch(() => { });
|
|
128
|
+
inPreloadFn && "then" in res && res.catch(() => { });
|
|
131
129
|
// serialize on server
|
|
132
130
|
if (isServer &&
|
|
133
131
|
sharedConfig.context &&
|
|
@@ -140,19 +138,19 @@ export function cache(fn, name) {
|
|
|
140
138
|
function handleResponse(error) {
|
|
141
139
|
return async (v) => {
|
|
142
140
|
if (v instanceof Response) {
|
|
143
|
-
|
|
144
|
-
|
|
141
|
+
const url = v.headers.get(LocationHeader);
|
|
142
|
+
if (url !== null) {
|
|
143
|
+
// client + server relative redirect
|
|
144
|
+
if (navigate && url.startsWith("/"))
|
|
145
145
|
startTransition(() => {
|
|
146
|
-
|
|
147
|
-
if (url && url.startsWith("/")) {
|
|
148
|
-
navigate(url, {
|
|
149
|
-
replace: true
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
else if (!isServer && url) {
|
|
153
|
-
window.location.href = url;
|
|
154
|
-
}
|
|
146
|
+
navigate(url, { replace: true });
|
|
155
147
|
});
|
|
148
|
+
else if (!isServer)
|
|
149
|
+
window.location.href = url;
|
|
150
|
+
else if (isServer) {
|
|
151
|
+
const e = getRequestEvent();
|
|
152
|
+
if (e)
|
|
153
|
+
e.response = { status: 302, headers: new Headers({ Location: url }) };
|
|
156
154
|
}
|
|
157
155
|
return;
|
|
158
156
|
}
|
package/dist/data/events.js
CHANGED
|
@@ -61,7 +61,7 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
|
|
|
61
61
|
url.pathname = transformUrl(url.pathname);
|
|
62
62
|
}
|
|
63
63
|
if (!preloadTimeout[url.pathname])
|
|
64
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
64
|
+
router.preloadRoute(url, { preloadData: a.getAttribute("preload") !== "false" });
|
|
65
65
|
}
|
|
66
66
|
function handleAnchorIn(evt) {
|
|
67
67
|
const res = handleAnchor(evt);
|
|
@@ -74,7 +74,7 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
|
|
|
74
74
|
if (preloadTimeout[url.pathname])
|
|
75
75
|
return;
|
|
76
76
|
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
77
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
77
|
+
router.preloadRoute(url, { preloadData: a.getAttribute("preload") !== "false" });
|
|
78
78
|
delete preloadTimeout[url.pathname];
|
|
79
79
|
}, 200);
|
|
80
80
|
}
|
|
@@ -111,10 +111,10 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
|
|
|
111
111
|
const handler = actions.get(actionRef);
|
|
112
112
|
if (handler) {
|
|
113
113
|
evt.preventDefault();
|
|
114
|
-
const data = new FormData(evt.target);
|
|
115
|
-
|
|
116
|
-
data
|
|
117
|
-
|
|
114
|
+
const data = new FormData(evt.target, evt.submitter);
|
|
115
|
+
handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
|
|
116
|
+
? data
|
|
117
|
+
: new URLSearchParams(data));
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
// ensure delegated event run first
|
package/dist/data/response.d.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
5
|
-
export declare function reload(init?: RouterResponseInit): never;
|
|
6
|
-
export declare function json<T>(data: T, init?: RouterResponseInit): T;
|
|
1
|
+
import type { RouterResponseInit, CustomResponse } from "../types";
|
|
2
|
+
export declare function redirect(url: string, init?: number | RouterResponseInit): CustomResponse<never>;
|
|
3
|
+
export declare function reload(init?: RouterResponseInit): CustomResponse<never>;
|
|
4
|
+
export declare function json<T>(data: T, init?: RouterResponseInit): CustomResponse<T>;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from "./routers/index.js";
|
|
2
2
|
export * from "./components.jsx";
|
|
3
3
|
export * from "./lifecycle.js";
|
|
4
|
-
export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing.js";
|
|
4
|
+
export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
|
|
5
5
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
6
6
|
export * from "./data/index.js";
|
|
7
|
-
export type { Location, LocationChange, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps,
|
|
7
|
+
export type { Location, LocationChange, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, BeforeLeaveEventArgs, RouteLoadFunc, RouteLoadFuncArgs, RouterResponseInit, CustomResponse } from "./types.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isServer, getRequestEvent, createComponent as createComponent$1, memo, delegateEvents, spread, mergeProps as mergeProps$1, template } from 'solid-js/web';
|
|
2
|
-
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, createComponent, children, mergeProps, Show, createRoot, getListener, sharedConfig, $TRACK, splitProps, createResource } from 'solid-js';
|
|
2
|
+
import { getOwner, runWithOwner, createMemo, createContext, onCleanup, useContext, untrack, createSignal, createRenderEffect, on, startTransition, resetErrorBoundaries, batch, createComponent, children, mergeProps, Show, createRoot, getListener, sharedConfig, $TRACK, splitProps, createResource } from 'solid-js';
|
|
3
3
|
import { createStore, reconcile, unwrap } from 'solid-js/store';
|
|
4
4
|
|
|
5
5
|
function createBeforeLeave() {
|
|
@@ -246,6 +246,7 @@ const useHref = to => {
|
|
|
246
246
|
const useNavigate = () => useRouter().navigatorFactory();
|
|
247
247
|
const useLocation = () => useRouter().location;
|
|
248
248
|
const useIsRouting = () => useRouter().isRouting;
|
|
249
|
+
const usePreloadRoute = () => useRouter().preloadRoute;
|
|
249
250
|
const useMatch = (path, matchFilters) => {
|
|
250
251
|
const location = useLocation();
|
|
251
252
|
const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
|
|
@@ -282,6 +283,7 @@ const useBeforeLeave = listener => {
|
|
|
282
283
|
function createRoutes(routeDef, base = "") {
|
|
283
284
|
const {
|
|
284
285
|
component,
|
|
286
|
+
preload,
|
|
285
287
|
load,
|
|
286
288
|
children,
|
|
287
289
|
info
|
|
@@ -290,7 +292,7 @@ function createRoutes(routeDef, base = "") {
|
|
|
290
292
|
const shared = {
|
|
291
293
|
key: routeDef,
|
|
292
294
|
component,
|
|
293
|
-
load,
|
|
295
|
+
preload: preload || load,
|
|
294
296
|
info
|
|
295
297
|
};
|
|
296
298
|
return asArray(routeDef.path).reduce((acc, originalPath) => {
|
|
@@ -407,12 +409,12 @@ let intent;
|
|
|
407
409
|
function getIntent() {
|
|
408
410
|
return intent;
|
|
409
411
|
}
|
|
410
|
-
let
|
|
411
|
-
function
|
|
412
|
-
return
|
|
412
|
+
let inPreloadFn = false;
|
|
413
|
+
function getInPreloadFn() {
|
|
414
|
+
return inPreloadFn;
|
|
413
415
|
}
|
|
414
|
-
function
|
|
415
|
-
|
|
416
|
+
function setInPreloadFn(value) {
|
|
417
|
+
inPreloadFn = value;
|
|
416
418
|
}
|
|
417
419
|
function createRouterContext(integration, branches, getContext, options = {}) {
|
|
418
420
|
const {
|
|
@@ -433,13 +435,33 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
433
435
|
});
|
|
434
436
|
}
|
|
435
437
|
const [isRouting, setIsRouting] = createSignal(false);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
438
|
+
|
|
439
|
+
// Keep track of last target, so that last call to transition wins
|
|
440
|
+
let lastTransitionTarget;
|
|
441
|
+
|
|
442
|
+
// Transition the location to a new value
|
|
443
|
+
const transition = (newIntent, newTarget) => {
|
|
444
|
+
if (newTarget.value === reference() && newTarget.state === state()) return;
|
|
445
|
+
if (lastTransitionTarget === undefined) setIsRouting(true);
|
|
446
|
+
intent = newIntent;
|
|
447
|
+
lastTransitionTarget = newTarget;
|
|
448
|
+
startTransition(() => {
|
|
449
|
+
if (lastTransitionTarget !== newTarget) return;
|
|
450
|
+
setReference(lastTransitionTarget.value);
|
|
451
|
+
setState(lastTransitionTarget.state);
|
|
452
|
+
resetErrorBoundaries();
|
|
453
|
+
if (!isServer) submissions[1]([]);
|
|
454
|
+
}).finally(() => {
|
|
455
|
+
if (lastTransitionTarget !== newTarget) return;
|
|
456
|
+
|
|
457
|
+
// Batch, in order for isRouting and final source update to happen together
|
|
458
|
+
batch(() => {
|
|
459
|
+
intent = undefined;
|
|
460
|
+
if (newIntent === "navigate") navigateEnd(lastTransitionTarget);
|
|
461
|
+
setIsRouting(false);
|
|
462
|
+
lastTransitionTarget = undefined;
|
|
463
|
+
});
|
|
464
|
+
});
|
|
443
465
|
};
|
|
444
466
|
const [reference, setReference] = createSignal(source().value);
|
|
445
467
|
const [state, setState] = createSignal(source().state);
|
|
@@ -468,24 +490,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
468
490
|
return resolvePath(basePath, to);
|
|
469
491
|
}
|
|
470
492
|
};
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
// Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
|
|
477
|
-
untrack(() => {
|
|
478
|
-
start(() => {
|
|
479
|
-
intent = "native";
|
|
480
|
-
if (value !== reference()) setReference(value);
|
|
481
|
-
setState(state);
|
|
482
|
-
resetErrorBoundaries();
|
|
483
|
-
submissions[1]([]);
|
|
484
|
-
}).then(() => {
|
|
485
|
-
intent = undefined;
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
});
|
|
493
|
+
|
|
494
|
+
// Create a native transition, when source updates
|
|
495
|
+
createRenderEffect(on(source, source => transition("native", source), {
|
|
496
|
+
defer: true
|
|
497
|
+
}));
|
|
489
498
|
return {
|
|
490
499
|
base: baseRoute,
|
|
491
500
|
location,
|
|
@@ -545,26 +554,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
545
554
|
state: nextState
|
|
546
555
|
});
|
|
547
556
|
} else if (beforeLeave.confirm(resolvedTo, options)) {
|
|
548
|
-
|
|
557
|
+
referrers.push({
|
|
549
558
|
value: current,
|
|
550
559
|
replace,
|
|
551
560
|
scroll,
|
|
552
561
|
state: state()
|
|
553
562
|
});
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
setState(nextState);
|
|
558
|
-
resetErrorBoundaries();
|
|
559
|
-
submissions[1]([]);
|
|
560
|
-
}).then(() => {
|
|
561
|
-
if (referrers.length === len) {
|
|
562
|
-
intent = undefined;
|
|
563
|
-
navigateEnd({
|
|
564
|
-
value: resolvedTo,
|
|
565
|
-
state: nextState
|
|
566
|
-
});
|
|
567
|
-
}
|
|
563
|
+
transition("navigate", {
|
|
564
|
+
value: resolvedTo,
|
|
565
|
+
state: nextState
|
|
568
566
|
});
|
|
569
567
|
}
|
|
570
568
|
}
|
|
@@ -578,17 +576,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
578
576
|
function navigateEnd(next) {
|
|
579
577
|
const first = referrers[0];
|
|
580
578
|
if (first) {
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
});
|
|
587
|
-
}
|
|
579
|
+
setSource({
|
|
580
|
+
...next,
|
|
581
|
+
replace: first.replace,
|
|
582
|
+
scroll: first.scroll
|
|
583
|
+
});
|
|
588
584
|
referrers.length = 0;
|
|
589
585
|
}
|
|
590
586
|
}
|
|
591
|
-
function preloadRoute(url,
|
|
587
|
+
function preloadRoute(url, options = {}) {
|
|
592
588
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
593
589
|
const prevIntent = intent;
|
|
594
590
|
intent = "preload";
|
|
@@ -599,10 +595,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
599
595
|
} = matches[match];
|
|
600
596
|
route.component && route.component.preload && route.component.preload();
|
|
601
597
|
const {
|
|
602
|
-
|
|
598
|
+
preload
|
|
603
599
|
} = route;
|
|
604
|
-
|
|
605
|
-
preloadData &&
|
|
600
|
+
inPreloadFn = true;
|
|
601
|
+
options.preloadData && preload && runWithOwner(getContext(), () => preload({
|
|
606
602
|
params,
|
|
607
603
|
location: {
|
|
608
604
|
pathname: url.pathname,
|
|
@@ -614,7 +610,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
614
610
|
},
|
|
615
611
|
intent: "preload"
|
|
616
612
|
}));
|
|
617
|
-
|
|
613
|
+
inPreloadFn = false;
|
|
618
614
|
}
|
|
619
615
|
intent = prevIntent;
|
|
620
616
|
}
|
|
@@ -632,17 +628,17 @@ function createRouteContext(router, parent, outlet, match) {
|
|
|
632
628
|
const {
|
|
633
629
|
pattern,
|
|
634
630
|
component,
|
|
635
|
-
|
|
631
|
+
preload
|
|
636
632
|
} = match().route;
|
|
637
633
|
const path = createMemo(() => match().path);
|
|
638
634
|
component && component.preload && component.preload();
|
|
639
|
-
|
|
640
|
-
const data =
|
|
635
|
+
inPreloadFn = true;
|
|
636
|
+
const data = preload ? preload({
|
|
641
637
|
params,
|
|
642
638
|
location,
|
|
643
639
|
intent: intent || "initial"
|
|
644
640
|
}) : undefined;
|
|
645
|
-
|
|
641
|
+
inPreloadFn = false;
|
|
646
642
|
const route = {
|
|
647
643
|
parent,
|
|
648
644
|
pattern,
|
|
@@ -683,8 +679,8 @@ const createRouterComponent = router => props => {
|
|
|
683
679
|
get root() {
|
|
684
680
|
return props.root;
|
|
685
681
|
},
|
|
686
|
-
get
|
|
687
|
-
return props.rootLoad;
|
|
682
|
+
get preload() {
|
|
683
|
+
return props.rootPreload || props.rootLoad;
|
|
688
684
|
},
|
|
689
685
|
get children() {
|
|
690
686
|
return [memo(() => (context = getOwner()) && null), createComponent$1(Routes, {
|
|
@@ -701,14 +697,14 @@ const createRouterComponent = router => props => {
|
|
|
701
697
|
function Root(props) {
|
|
702
698
|
const location = props.routerState.location;
|
|
703
699
|
const params = props.routerState.params;
|
|
704
|
-
const data = createMemo(() => props.
|
|
705
|
-
|
|
706
|
-
props.
|
|
700
|
+
const data = createMemo(() => props.preload && untrack(() => {
|
|
701
|
+
setInPreloadFn(true);
|
|
702
|
+
props.preload({
|
|
707
703
|
params,
|
|
708
704
|
location,
|
|
709
705
|
intent: getIntent() || "initial"
|
|
710
706
|
});
|
|
711
|
-
|
|
707
|
+
setInPreloadFn(false);
|
|
712
708
|
}));
|
|
713
709
|
return createComponent$1(Show, {
|
|
714
710
|
get when() {
|
|
@@ -813,7 +809,7 @@ function dataOnly(event, routerState, branches) {
|
|
|
813
809
|
route,
|
|
814
810
|
params
|
|
815
811
|
} = matches[match];
|
|
816
|
-
route.
|
|
812
|
+
route.preload && route.preload({
|
|
817
813
|
params,
|
|
818
814
|
location: routerState.location,
|
|
819
815
|
intent: "preload"
|
|
@@ -928,7 +924,7 @@ function cache(fn, name) {
|
|
|
928
924
|
const cachedFn = (...args) => {
|
|
929
925
|
const cache = getCache();
|
|
930
926
|
const intent = getIntent();
|
|
931
|
-
const
|
|
927
|
+
const inPreloadFn = getInPreloadFn();
|
|
932
928
|
const owner = getOwner();
|
|
933
929
|
const navigate = owner ? useNavigate() : undefined;
|
|
934
930
|
const now = Date.now();
|
|
@@ -962,10 +958,11 @@ function cache(fn, name) {
|
|
|
962
958
|
cached[0] = now;
|
|
963
959
|
}
|
|
964
960
|
let res = cached[1];
|
|
965
|
-
if (
|
|
961
|
+
if (intent !== "preload") {
|
|
966
962
|
res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
|
|
967
963
|
!isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
|
|
968
|
-
}
|
|
964
|
+
}
|
|
965
|
+
inPreloadFn && "then" in res && res.catch(() => {});
|
|
969
966
|
return res;
|
|
970
967
|
}
|
|
971
968
|
let res = !isServer && sharedConfig.context && sharedConfig.has(key) ? sharedConfig.load(key) // hydrating
|
|
@@ -987,9 +984,10 @@ function cache(fn, name) {
|
|
|
987
984
|
const e = getRequestEvent();
|
|
988
985
|
if (e && e.router.dataOnly) return e.router.data[key] = res;
|
|
989
986
|
}
|
|
990
|
-
if (
|
|
987
|
+
if (intent !== "preload") {
|
|
991
988
|
res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res);
|
|
992
|
-
}
|
|
989
|
+
}
|
|
990
|
+
inPreloadFn && "then" in res && res.catch(() => {});
|
|
993
991
|
// serialize on server
|
|
994
992
|
if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
|
|
995
993
|
const e = getRequestEvent();
|
|
@@ -999,18 +997,21 @@ function cache(fn, name) {
|
|
|
999
997
|
function handleResponse(error) {
|
|
1000
998
|
return async v => {
|
|
1001
999
|
if (v instanceof Response) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
replace: true
|
|
1009
|
-
});
|
|
1010
|
-
} else if (!isServer && url) {
|
|
1011
|
-
window.location.href = url;
|
|
1012
|
-
}
|
|
1000
|
+
const url = v.headers.get(LocationHeader);
|
|
1001
|
+
if (url !== null) {
|
|
1002
|
+
// client + server relative redirect
|
|
1003
|
+
if (navigate && url.startsWith("/")) startTransition(() => {
|
|
1004
|
+
navigate(url, {
|
|
1005
|
+
replace: true
|
|
1013
1006
|
});
|
|
1007
|
+
});else if (!isServer) window.location.href = url;else if (isServer) {
|
|
1008
|
+
const e = getRequestEvent();
|
|
1009
|
+
if (e) e.response = {
|
|
1010
|
+
status: 302,
|
|
1011
|
+
headers: new Headers({
|
|
1012
|
+
Location: url
|
|
1013
|
+
})
|
|
1014
|
+
};
|
|
1014
1015
|
}
|
|
1015
1016
|
return;
|
|
1016
1017
|
}
|
|
@@ -1075,6 +1076,7 @@ function useSubmission(fn, filter) {
|
|
|
1075
1076
|
const submissions = useSubmissions(fn, filter);
|
|
1076
1077
|
return new Proxy({}, {
|
|
1077
1078
|
get(_, property) {
|
|
1079
|
+
if (submissions.length === 0 && property === "clear" || property === "retry") return () => {};
|
|
1078
1080
|
return submissions[submissions.length - 1]?.[property];
|
|
1079
1081
|
}
|
|
1080
1082
|
});
|
|
@@ -1232,7 +1234,9 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
|
|
|
1232
1234
|
if (typeof transformUrl === "function") {
|
|
1233
1235
|
url.pathname = transformUrl(url.pathname);
|
|
1234
1236
|
}
|
|
1235
|
-
if (!preloadTimeout[url.pathname]) router.preloadRoute(url,
|
|
1237
|
+
if (!preloadTimeout[url.pathname]) router.preloadRoute(url, {
|
|
1238
|
+
preloadData: a.getAttribute("preload") !== "false"
|
|
1239
|
+
});
|
|
1236
1240
|
}
|
|
1237
1241
|
function handleAnchorIn(evt) {
|
|
1238
1242
|
const res = handleAnchor(evt);
|
|
@@ -1243,7 +1247,9 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
|
|
|
1243
1247
|
}
|
|
1244
1248
|
if (preloadTimeout[url.pathname]) return;
|
|
1245
1249
|
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
1246
|
-
router.preloadRoute(url,
|
|
1250
|
+
router.preloadRoute(url, {
|
|
1251
|
+
preloadData: a.getAttribute("preload") !== "false"
|
|
1252
|
+
});
|
|
1247
1253
|
delete preloadTimeout[url.pathname];
|
|
1248
1254
|
}, 200);
|
|
1249
1255
|
}
|
|
@@ -1273,12 +1279,11 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
|
|
|
1273
1279
|
const handler = actions.get(actionRef);
|
|
1274
1280
|
if (handler) {
|
|
1275
1281
|
evt.preventDefault();
|
|
1276
|
-
const data = new FormData(evt.target);
|
|
1277
|
-
if (evt.submitter && evt.submitter.name) data.append(evt.submitter.name, evt.submitter.value);
|
|
1282
|
+
const data = new FormData(evt.target, evt.submitter);
|
|
1278
1283
|
handler.call({
|
|
1279
1284
|
r: router,
|
|
1280
1285
|
f: evt.target
|
|
1281
|
-
}, data);
|
|
1286
|
+
}, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
|
|
1282
1287
|
}
|
|
1283
1288
|
}
|
|
1284
1289
|
|
|
@@ -1308,7 +1313,7 @@ function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "
|
|
|
1308
1313
|
function Router(props) {
|
|
1309
1314
|
if (isServer) return StaticRouter(props);
|
|
1310
1315
|
const getSource = () => {
|
|
1311
|
-
const url = window.location.pathname + window.location.search;
|
|
1316
|
+
const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
|
|
1312
1317
|
return {
|
|
1313
1318
|
value: props.transformUrl ? props.transformUrl(url) + window.location.hash : url + window.location.hash,
|
|
1314
1319
|
state: window.history.state
|
|
@@ -1328,7 +1333,7 @@ function Router(props) {
|
|
|
1328
1333
|
} else {
|
|
1329
1334
|
window.history.pushState(state, "", value);
|
|
1330
1335
|
}
|
|
1331
|
-
scrollToHash(window.location.hash.slice(1), scroll);
|
|
1336
|
+
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
1332
1337
|
saveCurrentDepth();
|
|
1333
1338
|
},
|
|
1334
1339
|
init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
|
|
@@ -1639,4 +1644,4 @@ function json(data, init = {}) {
|
|
|
1639
1644
|
return response;
|
|
1640
1645
|
}
|
|
1641
1646
|
|
|
1642
|
-
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|
|
1647
|
+
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|
package/dist/index.jsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export * from "./routers/index.js";
|
|
2
2
|
export * from "./components.jsx";
|
|
3
3
|
export * from "./lifecycle.js";
|
|
4
|
-
export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing.js";
|
|
4
|
+
export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute } from "./routing.js";
|
|
5
5
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
6
6
|
export * from "./data/index.js";
|
package/dist/routers/Router.js
CHANGED
|
@@ -7,7 +7,7 @@ export function Router(props) {
|
|
|
7
7
|
if (isServer)
|
|
8
8
|
return StaticRouter(props);
|
|
9
9
|
const getSource = () => {
|
|
10
|
-
const url = window.location.pathname + window.location.search;
|
|
10
|
+
const url = window.location.pathname.replace(/^\/+/, "/") + window.location.search;
|
|
11
11
|
return {
|
|
12
12
|
value: props.transformUrl ? props.transformUrl(url) + window.location.hash : url + window.location.hash,
|
|
13
13
|
state: window.history.state
|
|
@@ -23,7 +23,7 @@ export function Router(props) {
|
|
|
23
23
|
else {
|
|
24
24
|
window.history.pushState(state, "", value);
|
|
25
25
|
}
|
|
26
|
-
scrollToHash(window.location.hash.slice(1), scroll);
|
|
26
|
+
scrollToHash(decodeURIComponent(window.location.hash.slice(1)), scroll);
|
|
27
27
|
saveCurrentDepth();
|
|
28
28
|
},
|
|
29
29
|
init: notify => bindEvent(window, "popstate", notifyIfNotBlocked(notify, delta => {
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import type { Component, JSX } from "solid-js";
|
|
2
|
-
import type { MatchFilters,
|
|
2
|
+
import type { MatchFilters, RouteDefinition, RouterIntegration, RouteSectionProps, RoutePreloadFunc } from "../types.js";
|
|
3
3
|
export type BaseRouterProps = {
|
|
4
4
|
base?: string;
|
|
5
5
|
/**
|
|
6
6
|
* A component that wraps the content of every route.
|
|
7
7
|
*/
|
|
8
8
|
root?: Component<RouteSectionProps>;
|
|
9
|
-
|
|
9
|
+
rootPreload?: RoutePreloadFunc;
|
|
10
10
|
singleFlight?: boolean;
|
|
11
11
|
children?: JSX.Element | RouteDefinition | RouteDefinition[];
|
|
12
12
|
transformUrl?: (url: string) => string;
|
|
13
|
+
/** @deprecated use rootPreload */
|
|
14
|
+
rootLoad?: RoutePreloadFunc;
|
|
13
15
|
};
|
|
14
16
|
export declare const createRouterComponent: (router: RouterIntegration) => (props: BaseRouterProps) => JSX.Element;
|
|
15
17
|
export type RouteProps<S extends string, T = unknown> = {
|
|
16
18
|
path?: S | S[];
|
|
17
19
|
children?: JSX.Element;
|
|
18
|
-
|
|
20
|
+
preload?: RoutePreloadFunc<T>;
|
|
19
21
|
matchFilters?: MatchFilters<S>;
|
|
20
22
|
component?: Component<RouteSectionProps<T>>;
|
|
21
23
|
info?: Record<string, any>;
|
|
24
|
+
/** @deprecated use preload */
|
|
25
|
+
load?: RoutePreloadFunc<T>;
|
|
22
26
|
};
|
|
23
27
|
export declare const Route: <S extends string, T = unknown>(props: RouteProps<S, T>) => JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*@refresh skip*/
|
|
2
2
|
import { getRequestEvent, isServer } from "solid-js/web";
|
|
3
3
|
import { children, createMemo, createRoot, getOwner, mergeProps, on, Show, untrack } from "solid-js";
|
|
4
|
-
import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj,
|
|
4
|
+
import { createBranches, createRouteContext, createRouterContext, getIntent, getRouteMatches, RouteContextObj, RouterContextObj, setInPreloadFn } from "../routing.js";
|
|
5
5
|
export const createRouterComponent = (router) => (props) => {
|
|
6
6
|
const { base } = props;
|
|
7
7
|
const routeDefs = children(() => props.children);
|
|
@@ -14,7 +14,7 @@ export const createRouterComponent = (router) => (props) => {
|
|
|
14
14
|
});
|
|
15
15
|
router.create && router.create(routerState);
|
|
16
16
|
return (<RouterContextObj.Provider value={routerState}>
|
|
17
|
-
<Root routerState={routerState} root={props.root}
|
|
17
|
+
<Root routerState={routerState} root={props.root} preload={props.rootPreload || props.rootLoad}>
|
|
18
18
|
{(context = getOwner()) && null}
|
|
19
19
|
<Routes routerState={routerState} branches={branches()}/>
|
|
20
20
|
</Root>
|
|
@@ -23,11 +23,11 @@ export const createRouterComponent = (router) => (props) => {
|
|
|
23
23
|
function Root(props) {
|
|
24
24
|
const location = props.routerState.location;
|
|
25
25
|
const params = props.routerState.params;
|
|
26
|
-
const data = createMemo(() => props.
|
|
26
|
+
const data = createMemo(() => props.preload &&
|
|
27
27
|
untrack(() => {
|
|
28
|
-
|
|
29
|
-
props.
|
|
30
|
-
|
|
28
|
+
setInPreloadFn(true);
|
|
29
|
+
props.preload({ params, location, intent: getIntent() || "initial" });
|
|
30
|
+
setInPreloadFn(false);
|
|
31
31
|
}));
|
|
32
32
|
return (<Show when={props.root} keyed fallback={props.children}>
|
|
33
33
|
{Root => (<Root params={params} location={location} data={data()}>
|
|
@@ -105,8 +105,8 @@ function dataOnly(event, routerState, branches) {
|
|
|
105
105
|
if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
|
|
106
106
|
event.router.dataOnly = true;
|
|
107
107
|
const { route, params } = matches[match];
|
|
108
|
-
route.
|
|
109
|
-
route.
|
|
108
|
+
route.preload &&
|
|
109
|
+
route.preload({
|
|
110
110
|
params,
|
|
111
111
|
location: routerState.location,
|
|
112
112
|
intent: "preload"
|
package/dist/routing.d.ts
CHANGED
|
@@ -9,6 +9,9 @@ export declare const useHref: (to: () => string | undefined) => Accessor<string
|
|
|
9
9
|
export declare const useNavigate: () => Navigator;
|
|
10
10
|
export declare const useLocation: <S = unknown>() => Location<S>;
|
|
11
11
|
export declare const useIsRouting: () => () => boolean;
|
|
12
|
+
export declare const usePreloadRoute: () => (url: URL, options: {
|
|
13
|
+
preloadData?: boolean | undefined;
|
|
14
|
+
}) => void;
|
|
12
15
|
export declare const useMatch: <S extends string>(path: () => S, matchFilters?: MatchFilters<S> | undefined) => Accessor<import("./types.js").PathMatch | undefined>;
|
|
13
16
|
export declare const useCurrentMatches: () => () => RouteMatch[];
|
|
14
17
|
export declare const useParams: <T extends Params>() => T;
|
|
@@ -20,8 +23,8 @@ export declare function createBranches(routeDef: RouteDefinition | RouteDefiniti
|
|
|
20
23
|
export declare function getRouteMatches(branches: Branch[], location: string): RouteMatch[];
|
|
21
24
|
export declare function createLocation(path: Accessor<string>, state: Accessor<any>): Location;
|
|
22
25
|
export declare function getIntent(): Intent | undefined;
|
|
23
|
-
export declare function
|
|
24
|
-
export declare function
|
|
26
|
+
export declare function getInPreloadFn(): boolean;
|
|
27
|
+
export declare function setInPreloadFn(value: boolean): void;
|
|
25
28
|
export declare function createRouterContext(integration: RouterIntegration, branches: () => Branch[], getContext?: () => any, options?: {
|
|
26
29
|
base?: string;
|
|
27
30
|
singleFlight?: boolean;
|
package/dist/routing.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { runWithOwner } from "solid-js";
|
|
1
|
+
import { runWithOwner, batch } from "solid-js";
|
|
2
2
|
import { createComponent, createContext, createMemo, createRenderEffect, createSignal, on, onCleanup, untrack, useContext, startTransition, resetErrorBoundaries } from "solid-js";
|
|
3
3
|
import { isServer, getRequestEvent } from "solid-js/web";
|
|
4
4
|
import { createBeforeLeave } from "./lifecycle.js";
|
|
@@ -23,6 +23,7 @@ export const useHref = (to) => {
|
|
|
23
23
|
export const useNavigate = () => useRouter().navigatorFactory();
|
|
24
24
|
export const useLocation = () => useRouter().location;
|
|
25
25
|
export const useIsRouting = () => useRouter().isRouting;
|
|
26
|
+
export const usePreloadRoute = () => useRouter().preloadRoute;
|
|
26
27
|
export const useMatch = (path, matchFilters) => {
|
|
27
28
|
const location = useLocation();
|
|
28
29
|
const matchers = createMemo(() => expandOptionals(path()).map(path => createMatcher(path, undefined, matchFilters)));
|
|
@@ -58,12 +59,12 @@ export const useBeforeLeave = (listener) => {
|
|
|
58
59
|
onCleanup(s);
|
|
59
60
|
};
|
|
60
61
|
export function createRoutes(routeDef, base = "") {
|
|
61
|
-
const { component, load, children, info } = routeDef;
|
|
62
|
+
const { component, preload, load, children, info } = routeDef;
|
|
62
63
|
const isLeaf = !children || (Array.isArray(children) && !children.length);
|
|
63
64
|
const shared = {
|
|
64
65
|
key: routeDef,
|
|
65
66
|
component,
|
|
66
|
-
load,
|
|
67
|
+
preload: preload || load,
|
|
67
68
|
info
|
|
68
69
|
};
|
|
69
70
|
return asArray(routeDef.path).reduce((acc, originalPath) => {
|
|
@@ -185,12 +186,12 @@ let intent;
|
|
|
185
186
|
export function getIntent() {
|
|
186
187
|
return intent;
|
|
187
188
|
}
|
|
188
|
-
let
|
|
189
|
-
export function
|
|
190
|
-
return
|
|
189
|
+
let inPreloadFn = false;
|
|
190
|
+
export function getInPreloadFn() {
|
|
191
|
+
return inPreloadFn;
|
|
191
192
|
}
|
|
192
|
-
export function
|
|
193
|
-
|
|
193
|
+
export function setInPreloadFn(value) {
|
|
194
|
+
inPreloadFn = value;
|
|
194
195
|
}
|
|
195
196
|
export function createRouterContext(integration, branches, getContext, options = {}) {
|
|
196
197
|
const { signal: [source, setSource], utils = {} } = integration;
|
|
@@ -205,14 +206,36 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
205
206
|
setSource({ value: basePath, replace: true, scroll: false });
|
|
206
207
|
}
|
|
207
208
|
const [isRouting, setIsRouting] = createSignal(false);
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
209
|
+
// Keep track of last target, so that last call to transition wins
|
|
210
|
+
let lastTransitionTarget;
|
|
211
|
+
// Transition the location to a new value
|
|
212
|
+
const transition = (newIntent, newTarget) => {
|
|
213
|
+
if (newTarget.value === reference() && newTarget.state === state())
|
|
214
|
+
return;
|
|
215
|
+
if (lastTransitionTarget === undefined)
|
|
216
|
+
setIsRouting(true);
|
|
217
|
+
intent = newIntent;
|
|
218
|
+
lastTransitionTarget = newTarget;
|
|
219
|
+
startTransition(() => {
|
|
220
|
+
if (lastTransitionTarget !== newTarget)
|
|
221
|
+
return;
|
|
222
|
+
setReference(lastTransitionTarget.value);
|
|
223
|
+
setState(lastTransitionTarget.state);
|
|
224
|
+
resetErrorBoundaries();
|
|
225
|
+
if (!isServer)
|
|
226
|
+
submissions[1]([]);
|
|
227
|
+
}).finally(() => {
|
|
228
|
+
if (lastTransitionTarget !== newTarget)
|
|
229
|
+
return;
|
|
230
|
+
// Batch, in order for isRouting and final source update to happen together
|
|
231
|
+
batch(() => {
|
|
232
|
+
intent = undefined;
|
|
233
|
+
if (newIntent === "navigate")
|
|
234
|
+
navigateEnd(lastTransitionTarget);
|
|
235
|
+
setIsRouting(false);
|
|
236
|
+
lastTransitionTarget = undefined;
|
|
237
|
+
});
|
|
238
|
+
});
|
|
216
239
|
};
|
|
217
240
|
const [reference, setReference] = createSignal(source().value);
|
|
218
241
|
const [state, setState] = createSignal(source().state);
|
|
@@ -241,22 +264,8 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
241
264
|
return resolvePath(basePath, to);
|
|
242
265
|
}
|
|
243
266
|
};
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// Untrack this whole block so `start` doesn't cause Solid's Listener to be preserved
|
|
247
|
-
untrack(() => {
|
|
248
|
-
start(() => {
|
|
249
|
-
intent = "native";
|
|
250
|
-
if (value !== reference())
|
|
251
|
-
setReference(value);
|
|
252
|
-
setState(state);
|
|
253
|
-
resetErrorBoundaries();
|
|
254
|
-
submissions[1]([]);
|
|
255
|
-
}).then(() => {
|
|
256
|
-
intent = undefined;
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
});
|
|
267
|
+
// Create a native transition, when source updates
|
|
268
|
+
createRenderEffect(on(source, source => transition("native", source), { defer: true }));
|
|
260
269
|
return {
|
|
261
270
|
base: baseRoute,
|
|
262
271
|
location,
|
|
@@ -307,21 +316,10 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
307
316
|
setSource({ value: resolvedTo, replace, scroll, state: nextState });
|
|
308
317
|
}
|
|
309
318
|
else if (beforeLeave.confirm(resolvedTo, options)) {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
setState(nextState);
|
|
315
|
-
resetErrorBoundaries();
|
|
316
|
-
submissions[1]([]);
|
|
317
|
-
}).then(() => {
|
|
318
|
-
if (referrers.length === len) {
|
|
319
|
-
intent = undefined;
|
|
320
|
-
navigateEnd({
|
|
321
|
-
value: resolvedTo,
|
|
322
|
-
state: nextState
|
|
323
|
-
});
|
|
324
|
-
}
|
|
319
|
+
referrers.push({ value: current, replace, scroll, state: state() });
|
|
320
|
+
transition("navigate", {
|
|
321
|
+
value: resolvedTo,
|
|
322
|
+
state: nextState
|
|
325
323
|
});
|
|
326
324
|
}
|
|
327
325
|
}
|
|
@@ -335,17 +333,15 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
335
333
|
function navigateEnd(next) {
|
|
336
334
|
const first = referrers[0];
|
|
337
335
|
if (first) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
});
|
|
344
|
-
}
|
|
336
|
+
setSource({
|
|
337
|
+
...next,
|
|
338
|
+
replace: first.replace,
|
|
339
|
+
scroll: first.scroll
|
|
340
|
+
});
|
|
345
341
|
referrers.length = 0;
|
|
346
342
|
}
|
|
347
343
|
}
|
|
348
|
-
function preloadRoute(url,
|
|
344
|
+
function preloadRoute(url, options = {}) {
|
|
349
345
|
const matches = getRouteMatches(branches(), url.pathname);
|
|
350
346
|
const prevIntent = intent;
|
|
351
347
|
intent = "preload";
|
|
@@ -354,11 +350,11 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
354
350
|
route.component &&
|
|
355
351
|
route.component.preload &&
|
|
356
352
|
route.component.preload();
|
|
357
|
-
const {
|
|
358
|
-
|
|
359
|
-
preloadData &&
|
|
360
|
-
|
|
361
|
-
runWithOwner(getContext(), () =>
|
|
353
|
+
const { preload } = route;
|
|
354
|
+
inPreloadFn = true;
|
|
355
|
+
options.preloadData &&
|
|
356
|
+
preload &&
|
|
357
|
+
runWithOwner(getContext(), () => preload({
|
|
362
358
|
params,
|
|
363
359
|
location: {
|
|
364
360
|
pathname: url.pathname,
|
|
@@ -370,27 +366,25 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
370
366
|
},
|
|
371
367
|
intent: "preload"
|
|
372
368
|
}));
|
|
373
|
-
|
|
369
|
+
inPreloadFn = false;
|
|
374
370
|
}
|
|
375
371
|
intent = prevIntent;
|
|
376
372
|
}
|
|
377
373
|
function initFromFlash() {
|
|
378
374
|
const e = getRequestEvent();
|
|
379
|
-
return (e && e.router && e.router.submission
|
|
380
|
-
? [e.router.submission]
|
|
381
|
-
: []);
|
|
375
|
+
return (e && e.router && e.router.submission ? [e.router.submission] : []);
|
|
382
376
|
}
|
|
383
377
|
}
|
|
384
378
|
export function createRouteContext(router, parent, outlet, match) {
|
|
385
379
|
const { base, location, params } = router;
|
|
386
|
-
const { pattern, component,
|
|
380
|
+
const { pattern, component, preload } = match().route;
|
|
387
381
|
const path = createMemo(() => match().path);
|
|
388
382
|
component &&
|
|
389
383
|
component.preload &&
|
|
390
384
|
component.preload();
|
|
391
|
-
|
|
392
|
-
const data =
|
|
393
|
-
|
|
385
|
+
inPreloadFn = true;
|
|
386
|
+
const data = preload ? preload({ params, location, intent: intent || "initial" }) : undefined;
|
|
387
|
+
inPreloadFn = false;
|
|
394
388
|
const route = {
|
|
395
389
|
parent,
|
|
396
390
|
pattern,
|
package/dist/types.d.ts
CHANGED
|
@@ -56,12 +56,12 @@ export interface RouterIntegration {
|
|
|
56
56
|
utils?: Partial<RouterUtils>;
|
|
57
57
|
}
|
|
58
58
|
export type Intent = "initial" | "native" | "navigate" | "preload";
|
|
59
|
-
export interface
|
|
59
|
+
export interface RoutePreloadFuncArgs {
|
|
60
60
|
params: Params;
|
|
61
61
|
location: Location;
|
|
62
62
|
intent: Intent;
|
|
63
63
|
}
|
|
64
|
-
export type
|
|
64
|
+
export type RoutePreloadFunc<T = unknown> = (args: RoutePreloadFuncArgs) => T;
|
|
65
65
|
export interface RouteSectionProps<T = unknown> {
|
|
66
66
|
params: Params;
|
|
67
67
|
location: Location;
|
|
@@ -71,10 +71,12 @@ export interface RouteSectionProps<T = unknown> {
|
|
|
71
71
|
export type RouteDefinition<S extends string | string[] = any, T = unknown> = {
|
|
72
72
|
path?: S;
|
|
73
73
|
matchFilters?: MatchFilters<S>;
|
|
74
|
-
|
|
74
|
+
preload?: RoutePreloadFunc<T>;
|
|
75
75
|
children?: RouteDefinition | RouteDefinition[];
|
|
76
76
|
component?: Component<RouteSectionProps<T>>;
|
|
77
77
|
info?: Record<string, any>;
|
|
78
|
+
/** @deprecated use preload */
|
|
79
|
+
load?: RoutePreloadFunc;
|
|
78
80
|
};
|
|
79
81
|
export type MatchFilter = readonly string[] | RegExp | ((s: string) => boolean);
|
|
80
82
|
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] : [];
|
|
@@ -100,7 +102,7 @@ export interface RouteDescription {
|
|
|
100
102
|
originalPath: string;
|
|
101
103
|
pattern: string;
|
|
102
104
|
component?: Component<RouteSectionProps>;
|
|
103
|
-
|
|
105
|
+
preload?: RoutePreloadFunc;
|
|
104
106
|
matcher: (location: string) => PathMatch | null;
|
|
105
107
|
matchFilters?: MatchFilters;
|
|
106
108
|
info?: Record<string, any>;
|
|
@@ -134,7 +136,9 @@ export interface RouterContext {
|
|
|
134
136
|
renderPath(path: string): string;
|
|
135
137
|
parsePath(str: string): string;
|
|
136
138
|
beforeLeave: BeforeLeaveLifecycle;
|
|
137
|
-
preloadRoute: (url: URL,
|
|
139
|
+
preloadRoute: (url: URL, options: {
|
|
140
|
+
preloadData?: boolean;
|
|
141
|
+
}) => void;
|
|
138
142
|
singleFlight: boolean;
|
|
139
143
|
submissions: Signal<Submission<any, any>[]>;
|
|
140
144
|
}
|
|
@@ -164,9 +168,30 @@ export type Submission<T, U> = {
|
|
|
164
168
|
clear: () => void;
|
|
165
169
|
retry: () => void;
|
|
166
170
|
};
|
|
171
|
+
export type SubmissionStub = {
|
|
172
|
+
readonly input: undefined;
|
|
173
|
+
readonly result: undefined;
|
|
174
|
+
readonly error: undefined;
|
|
175
|
+
readonly pending: undefined;
|
|
176
|
+
readonly url: undefined;
|
|
177
|
+
clear: () => void;
|
|
178
|
+
retry: () => void;
|
|
179
|
+
};
|
|
167
180
|
export interface MaybePreloadableComponent extends Component {
|
|
168
181
|
preload?: () => void;
|
|
169
182
|
}
|
|
170
183
|
export type CacheEntry = [number, any, Intent | undefined, Signal<number> & {
|
|
171
184
|
count: number;
|
|
172
185
|
}];
|
|
186
|
+
export type NarrowResponse<T> = T extends CustomResponse<infer U> ? U : Exclude<T, Response>;
|
|
187
|
+
export type RouterResponseInit = Omit<ResponseInit, "body"> & {
|
|
188
|
+
revalidate?: string | string[];
|
|
189
|
+
};
|
|
190
|
+
export type CustomResponse<T> = Omit<Response, "clone"> & {
|
|
191
|
+
customBody: () => T;
|
|
192
|
+
clone(...args: readonly unknown[]): CustomResponse<T>;
|
|
193
|
+
};
|
|
194
|
+
/** @deprecated */
|
|
195
|
+
export type RouteLoadFunc = RoutePreloadFunc;
|
|
196
|
+
/** @deprecated */
|
|
197
|
+
export type RouteLoadFuncArgs = RoutePreloadFuncArgs;
|