@solidjs/router 0.10.0-beta.8 → 0.10.0-beta.9
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 +54 -5
- package/dist/components.d.ts +1 -0
- package/dist/components.jsx +1 -1
- package/dist/data/action.d.ts +1 -1
- package/dist/data/cache.d.ts +3 -0
- package/dist/data/cache.js +26 -2
- package/dist/data/events.d.ts +1 -1
- package/dist/data/events.js +117 -109
- package/dist/index.js +121 -95
- package/dist/routers/HashRouter.d.ts +5 -1
- package/dist/routers/HashRouter.js +1 -1
- package/dist/routers/Router.d.ts +3 -0
- package/dist/routers/Router.js +1 -1
- package/dist/routers/components.d.ts +0 -1
- package/dist/routers/components.jsx +2 -2
- package/dist/routing.d.ts +0 -1
- package/dist/routing.js +0 -1
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -368,6 +368,14 @@ This cache accomplishes the following:
|
|
|
368
368
|
3. We have a reactive refetch mechanism based on key. So we can tell routes that aren't new to retrigger on action revalidation.
|
|
369
369
|
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.
|
|
370
370
|
|
|
371
|
+
Cached function has a few useful methods for getting the key that are useful for invalidation.
|
|
372
|
+
```ts
|
|
373
|
+
let id = 5;
|
|
374
|
+
|
|
375
|
+
getUser.key // returns "users"
|
|
376
|
+
getUser.keyFor(id) // returns "users[5]"
|
|
377
|
+
```
|
|
378
|
+
|
|
371
379
|
This cache can be defined anywhere and then used inside your components with:
|
|
372
380
|
|
|
373
381
|
### `createAsync`
|
|
@@ -378,6 +386,8 @@ This is light wrapper over `createResource` that aims to serve as stand-in for a
|
|
|
378
386
|
const user = createAsync(() => getUser(params.id))
|
|
379
387
|
```
|
|
380
388
|
|
|
389
|
+
Using `cache` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
|
|
390
|
+
|
|
381
391
|
### `action`
|
|
382
392
|
|
|
383
393
|
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response builders can be found below(TODO).
|
|
@@ -391,12 +401,38 @@ const myAction = action(async (data) => {
|
|
|
391
401
|
});
|
|
392
402
|
|
|
393
403
|
// in component
|
|
394
|
-
<form action={myAction} />
|
|
404
|
+
<form action={myAction} method="post" />
|
|
395
405
|
|
|
396
406
|
//or
|
|
397
407
|
<button type="submit" formaction={myAction}></button>
|
|
398
408
|
```
|
|
399
409
|
|
|
410
|
+
Actions only work with post requests, so make sure to put `method="post"` on your form.
|
|
411
|
+
|
|
412
|
+
Sometimes it might be easier to deal with typed data instead of `FormData` and adding additional hidden fields. For that reason Actions have a with method. That works similar to `bind` which applies the arguments in order.
|
|
413
|
+
|
|
414
|
+
Picture an action that deletes Todo Item:
|
|
415
|
+
|
|
416
|
+
```js
|
|
417
|
+
const deleteTodo = action(async (formData: FormData) => {
|
|
418
|
+
const id = Number(formData.get("id"))
|
|
419
|
+
await api.deleteTodo(id)
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
<form action={deleteUser} method="post">
|
|
423
|
+
<input type="hidden" name="id" value={todo.id} />
|
|
424
|
+
<button type="submit">Delete</button>
|
|
425
|
+
</form>
|
|
426
|
+
```
|
|
427
|
+
Instead with `with` you can write this:
|
|
428
|
+
```js
|
|
429
|
+
const deleteUser = action(api.deleteUser)
|
|
430
|
+
|
|
431
|
+
<form action={deleteUser.with(todo.id)} method="post">
|
|
432
|
+
<button type="submit">Delete</button>
|
|
433
|
+
</form>
|
|
434
|
+
```
|
|
435
|
+
|
|
400
436
|
#### Notes of `<form>` implementation and SSR
|
|
401
437
|
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.
|
|
402
438
|
|
|
@@ -571,9 +607,22 @@ import { Router } from "@solidjs/router";
|
|
|
571
607
|
|
|
572
608
|
## Components
|
|
573
609
|
|
|
610
|
+
### `<Router>`
|
|
611
|
+
|
|
612
|
+
This is the main Router component for the browser.
|
|
613
|
+
|
|
614
|
+
| prop | type | description |
|
|
615
|
+
|-----|----|----|
|
|
616
|
+
| children | `JSX.Element` or `RouteDefinition[]` | The route definitions |
|
|
617
|
+
| root | Component | Top level layout comoponent |
|
|
618
|
+
| base | string | Base url to use for matching routes |
|
|
619
|
+
| actionBase | string | Root url for server actions, default: `/_server` |
|
|
620
|
+
| preload | boolean | Enables/disables preloads globally, default: `true` |
|
|
621
|
+
| explicitLinks | boolean | Disables all anchors being intercepted and instead requires `<A>`. default: `false` |
|
|
622
|
+
|
|
574
623
|
### `<A>`
|
|
575
624
|
|
|
576
|
-
Like the `<a>` tag but supports relative paths and active class styling.
|
|
625
|
+
Like the `<a>` tag but supports relative paths and active class styling (requires client side JavaScript).
|
|
577
626
|
|
|
578
627
|
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.
|
|
579
628
|
|
|
@@ -729,9 +778,9 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
|
729
778
|
});
|
|
730
779
|
```
|
|
731
780
|
|
|
732
|
-
## Migrations from 0.
|
|
781
|
+
## Migrations from 0.9.x
|
|
733
782
|
|
|
734
|
-
v0.
|
|
783
|
+
v0.10.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.
|
|
735
784
|
|
|
736
785
|
The biggest changes are around removed APIs that need to be replaced.
|
|
737
786
|
|
|
@@ -745,7 +794,7 @@ Related without Outlet component it has to be passed in manually. At which point
|
|
|
745
794
|
|
|
746
795
|
### `data` functions & `useRouteData`
|
|
747
796
|
|
|
748
|
-
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.
|
|
797
|
+
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.
|
|
749
798
|
|
|
750
799
|
## SPAs in Deployed Environments
|
|
751
800
|
|
package/dist/components.d.ts
CHANGED
package/dist/components.jsx
CHANGED
|
@@ -27,7 +27,7 @@ export function A(props) {
|
|
|
27
27
|
[props.inactiveClass]: !isActive(),
|
|
28
28
|
[props.activeClass]: isActive(),
|
|
29
29
|
...rest.classList
|
|
30
|
-
}} aria-current={isActive() ? "page" : undefined}/>);
|
|
30
|
+
}} link aria-current={isActive() ? "page" : undefined}/>);
|
|
31
31
|
}
|
|
32
32
|
export function Navigate(props) {
|
|
33
33
|
const navigate = useNavigate();
|
package/dist/data/action.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { JSX } from "solid-js";
|
|
2
2
|
import { Submission } from "../types";
|
|
3
|
-
export type Action<T extends Array<any>, U> = ((...vars: T) => Promise<U>) &
|
|
3
|
+
export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<U>) & {
|
|
4
4
|
url: string;
|
|
5
5
|
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => U, ...args: A): Action<B, U>;
|
|
6
6
|
};
|
package/dist/data/cache.d.ts
CHANGED
|
@@ -5,4 +5,7 @@ export type CachedFunction<T extends (...args: any) => U | Response, U> = T & {
|
|
|
5
5
|
key: string;
|
|
6
6
|
};
|
|
7
7
|
export declare function cache<T extends (...args: any) => U | Response, U>(fn: T, name: string, options?: ReconcileOptions): CachedFunction<T, U>;
|
|
8
|
+
export declare namespace cache {
|
|
9
|
+
var set: (key: string, value: any) => void;
|
|
10
|
+
}
|
|
8
11
|
export declare function hashKey<T extends Array<any>>(args: T): string;
|
package/dist/data/cache.js
CHANGED
|
@@ -22,6 +22,8 @@ function getCache() {
|
|
|
22
22
|
if (!isServer)
|
|
23
23
|
return cacheMap;
|
|
24
24
|
const req = getRequestEvent() || sharedConfig.context;
|
|
25
|
+
if (!req)
|
|
26
|
+
throw new Error("Cannot find cache context");
|
|
25
27
|
return req.routerCache || (req.routerCache = new Map());
|
|
26
28
|
}
|
|
27
29
|
export function revalidate(key) {
|
|
@@ -78,8 +80,9 @@ export function cache(fn, name, options) {
|
|
|
78
80
|
? sharedConfig.load(key) // hydrating
|
|
79
81
|
: fn(...args);
|
|
80
82
|
// serialize on server
|
|
81
|
-
if (isServer && sharedConfig.context && !sharedConfig.context.noHydrate) {
|
|
82
|
-
|
|
83
|
+
if (isServer && (sharedConfig.context && !sharedConfig.context.noHydrate)) {
|
|
84
|
+
const e = getRequestEvent();
|
|
85
|
+
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
83
86
|
}
|
|
84
87
|
if (cached) {
|
|
85
88
|
cached[0] = now;
|
|
@@ -130,6 +133,27 @@ export function cache(fn, name, options) {
|
|
|
130
133
|
cachedFn.key = name;
|
|
131
134
|
return cachedFn;
|
|
132
135
|
}
|
|
136
|
+
;
|
|
137
|
+
cache.set = (key, value) => {
|
|
138
|
+
const cache = getCache();
|
|
139
|
+
const now = Date.now();
|
|
140
|
+
let cached = cache.get(key);
|
|
141
|
+
let version;
|
|
142
|
+
if (getOwner()) {
|
|
143
|
+
version = createSignal(now, {
|
|
144
|
+
equals: (p, v) => v - p < 50 // margin of error
|
|
145
|
+
});
|
|
146
|
+
onCleanup(() => cached[3].delete(version));
|
|
147
|
+
}
|
|
148
|
+
if (cached) {
|
|
149
|
+
cached[0] = now;
|
|
150
|
+
cached[1] = value;
|
|
151
|
+
cached[2] = "preload";
|
|
152
|
+
version && cached[3].add(version);
|
|
153
|
+
}
|
|
154
|
+
else
|
|
155
|
+
cache.set(key, (cached = [now, value, , new Set(version ? [version] : [])]));
|
|
156
|
+
};
|
|
133
157
|
function matchKey(key, keys) {
|
|
134
158
|
for (let k of keys) {
|
|
135
159
|
if (key.startsWith(k))
|
package/dist/data/events.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { RouterContext } from "../types";
|
|
2
|
-
export declare function setupNativeEvents(router: RouterContext)
|
|
2
|
+
export declare function setupNativeEvents(preload?: boolean, explicitLinks?: boolean, actionBase?: string): (router: RouterContext) => void;
|
package/dist/data/events.js
CHANGED
|
@@ -1,118 +1,126 @@
|
|
|
1
1
|
import { delegateEvents } from "solid-js/web";
|
|
2
2
|
import { onCleanup } from "solid-js";
|
|
3
3
|
import { actions } from "./action";
|
|
4
|
-
export function setupNativeEvents(
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
function handleAnchor(evt) {
|
|
12
|
-
if (evt.defaultPrevented ||
|
|
13
|
-
evt.button !== 0 ||
|
|
14
|
-
evt.metaKey ||
|
|
15
|
-
evt.altKey ||
|
|
16
|
-
evt.ctrlKey ||
|
|
17
|
-
evt.shiftKey)
|
|
18
|
-
return;
|
|
19
|
-
const a = evt
|
|
20
|
-
.composedPath()
|
|
21
|
-
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
22
|
-
if (!a)
|
|
23
|
-
return;
|
|
24
|
-
const svg = isSvg(a);
|
|
25
|
-
const href = svg ? a.href.baseVal : a.href;
|
|
26
|
-
const target = svg ? a.target.baseVal : a.target;
|
|
27
|
-
if (target || (!href && !a.hasAttribute("state")))
|
|
28
|
-
return;
|
|
29
|
-
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
30
|
-
if (a.hasAttribute("download") || (rel && rel.includes("external")))
|
|
31
|
-
return;
|
|
32
|
-
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
33
|
-
if (url.origin !== window.location.origin ||
|
|
34
|
-
(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
|
|
35
|
-
return;
|
|
36
|
-
return [a, url];
|
|
37
|
-
}
|
|
38
|
-
function handleAnchorClick(evt) {
|
|
39
|
-
const res = handleAnchor(evt);
|
|
40
|
-
if (!res)
|
|
41
|
-
return;
|
|
42
|
-
const [a, url] = res;
|
|
43
|
-
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
44
|
-
const state = a.getAttribute("state");
|
|
45
|
-
evt.preventDefault();
|
|
46
|
-
navigateFromRoute(to, {
|
|
47
|
-
resolve: false,
|
|
48
|
-
replace: a.hasAttribute("replace"),
|
|
49
|
-
scroll: !a.hasAttribute("noscroll"),
|
|
50
|
-
state: state && JSON.parse(state)
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
function handleAnchorPreload(evt) {
|
|
54
|
-
const res = handleAnchor(evt);
|
|
55
|
-
if (!res)
|
|
56
|
-
return;
|
|
57
|
-
const [a, url] = res;
|
|
58
|
-
if (!preloadTimeout[url.pathname])
|
|
59
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
60
|
-
}
|
|
61
|
-
function handleAnchorIn(evt) {
|
|
62
|
-
const res = handleAnchor(evt);
|
|
63
|
-
if (!res)
|
|
64
|
-
return;
|
|
65
|
-
const [a, url] = res;
|
|
66
|
-
if (preloadTimeout[url.pathname])
|
|
67
|
-
return;
|
|
68
|
-
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
69
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
70
|
-
delete preloadTimeout[url.pathname];
|
|
71
|
-
}, 200);
|
|
72
|
-
}
|
|
73
|
-
function handleAnchorOut(evt) {
|
|
74
|
-
const res = handleAnchor(evt);
|
|
75
|
-
if (!res)
|
|
76
|
-
return;
|
|
77
|
-
const [, url] = res;
|
|
78
|
-
if (preloadTimeout[url.pathname]) {
|
|
79
|
-
clearTimeout(preloadTimeout[url.pathname]);
|
|
80
|
-
delete preloadTimeout[url.pathname];
|
|
4
|
+
export function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") {
|
|
5
|
+
return (router) => {
|
|
6
|
+
const basePath = router.base.path();
|
|
7
|
+
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
8
|
+
let preloadTimeout = {};
|
|
9
|
+
function isSvg(el) {
|
|
10
|
+
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
81
11
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
12
|
+
function handleAnchor(evt) {
|
|
13
|
+
if (evt.defaultPrevented ||
|
|
14
|
+
evt.button !== 0 ||
|
|
15
|
+
evt.metaKey ||
|
|
16
|
+
evt.altKey ||
|
|
17
|
+
evt.ctrlKey ||
|
|
18
|
+
evt.shiftKey)
|
|
19
|
+
return;
|
|
20
|
+
const a = evt
|
|
21
|
+
.composedPath()
|
|
22
|
+
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
23
|
+
if (!a || (explicitLinks && !a.getAttribute("link")))
|
|
24
|
+
return;
|
|
25
|
+
const svg = isSvg(a);
|
|
26
|
+
const href = svg ? a.href.baseVal : a.href;
|
|
27
|
+
const target = svg ? a.target.baseVal : a.target;
|
|
28
|
+
if (target || (!href && !a.hasAttribute("state")))
|
|
29
|
+
return;
|
|
30
|
+
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
31
|
+
if (a.hasAttribute("download") || (rel && rel.includes("external")))
|
|
32
|
+
return;
|
|
33
|
+
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
34
|
+
if (url.origin !== window.location.origin ||
|
|
35
|
+
(basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
|
|
93
36
|
return;
|
|
37
|
+
return [a, url];
|
|
94
38
|
}
|
|
95
|
-
|
|
96
|
-
|
|
39
|
+
function handleAnchorClick(evt) {
|
|
40
|
+
const res = handleAnchor(evt);
|
|
41
|
+
if (!res)
|
|
42
|
+
return;
|
|
43
|
+
const [a, url] = res;
|
|
44
|
+
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
45
|
+
const state = a.getAttribute("state");
|
|
97
46
|
evt.preventDefault();
|
|
98
|
-
|
|
99
|
-
|
|
47
|
+
navigateFromRoute(to, {
|
|
48
|
+
resolve: false,
|
|
49
|
+
replace: a.hasAttribute("replace"),
|
|
50
|
+
scroll: !a.hasAttribute("noscroll"),
|
|
51
|
+
state: state && JSON.parse(state)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function handleAnchorPreload(evt) {
|
|
55
|
+
const res = handleAnchor(evt);
|
|
56
|
+
if (!res)
|
|
57
|
+
return;
|
|
58
|
+
const [a, url] = res;
|
|
59
|
+
if (!preloadTimeout[url.pathname])
|
|
60
|
+
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
100
61
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
62
|
+
function handleAnchorIn(evt) {
|
|
63
|
+
const res = handleAnchor(evt);
|
|
64
|
+
if (!res)
|
|
65
|
+
return;
|
|
66
|
+
const [a, url] = res;
|
|
67
|
+
if (preloadTimeout[url.pathname])
|
|
68
|
+
return;
|
|
69
|
+
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
70
|
+
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
71
|
+
delete preloadTimeout[url.pathname];
|
|
72
|
+
}, 200);
|
|
73
|
+
}
|
|
74
|
+
function handleAnchorOut(evt) {
|
|
75
|
+
const res = handleAnchor(evt);
|
|
76
|
+
if (!res)
|
|
77
|
+
return;
|
|
78
|
+
const [, url] = res;
|
|
79
|
+
if (preloadTimeout[url.pathname]) {
|
|
80
|
+
clearTimeout(preloadTimeout[url.pathname]);
|
|
81
|
+
delete preloadTimeout[url.pathname];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function handleFormSubmit(evt) {
|
|
85
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
|
|
86
|
+
? evt.submitter.formAction
|
|
87
|
+
: evt.target.action;
|
|
88
|
+
if (!actionRef)
|
|
89
|
+
return;
|
|
90
|
+
if (!actionRef.startsWith("action:")) {
|
|
91
|
+
const url = new URL(actionRef);
|
|
92
|
+
actionRef = router.parsePath(url.pathname + url.search);
|
|
93
|
+
if (!actionRef.startsWith(actionBase))
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (evt.target.method.toUpperCase() !== "POST")
|
|
97
|
+
throw new Error("Only POST forms are supported for Actions");
|
|
98
|
+
const handler = actions.get(actionRef);
|
|
99
|
+
if (handler) {
|
|
100
|
+
evt.preventDefault();
|
|
101
|
+
const data = new FormData(evt.target);
|
|
102
|
+
handler.call(router, data);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// ensure delegated event run first
|
|
106
|
+
delegateEvents(["click", "submit"]);
|
|
107
|
+
document.addEventListener("click", handleAnchorClick);
|
|
108
|
+
if (preload) {
|
|
109
|
+
document.addEventListener("mouseover", handleAnchorIn);
|
|
110
|
+
document.addEventListener("mouseout", handleAnchorOut);
|
|
111
|
+
document.addEventListener("focusin", handleAnchorPreload);
|
|
112
|
+
document.addEventListener("touchstart", handleAnchorPreload);
|
|
113
|
+
}
|
|
114
|
+
document.addEventListener("submit", handleFormSubmit);
|
|
115
|
+
onCleanup(() => {
|
|
116
|
+
document.removeEventListener("click", handleAnchorClick);
|
|
117
|
+
if (preload) {
|
|
118
|
+
document.removeEventListener("mouseover", handleAnchorIn);
|
|
119
|
+
document.removeEventListener("mouseout", handleAnchorOut);
|
|
120
|
+
document.removeEventListener("focusin", handleAnchorPreload);
|
|
121
|
+
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
122
|
+
}
|
|
123
|
+
document.removeEventListener("submit", handleFormSubmit);
|
|
124
|
+
});
|
|
125
|
+
};
|
|
118
126
|
}
|
package/dist/index.js
CHANGED
|
@@ -421,7 +421,6 @@ function createRouterContext(integration, getBranches, options = {}) {
|
|
|
421
421
|
});
|
|
422
422
|
return {
|
|
423
423
|
base: baseRoute,
|
|
424
|
-
actionBase: options.actionBase || "/_server",
|
|
425
424
|
location,
|
|
426
425
|
isRouting,
|
|
427
426
|
renderPath,
|
|
@@ -587,8 +586,7 @@ function createRouteContext(router, parent, outlet, match, params) {
|
|
|
587
586
|
|
|
588
587
|
const createRouterComponent = router => props => {
|
|
589
588
|
const {
|
|
590
|
-
base
|
|
591
|
-
actionBase
|
|
589
|
+
base
|
|
592
590
|
} = props;
|
|
593
591
|
const routeDefs = children(() => props.children);
|
|
594
592
|
const branches = createMemo(() => createBranches(props.root ? {
|
|
@@ -596,8 +594,7 @@ const createRouterComponent = router => props => {
|
|
|
596
594
|
children: routeDefs()
|
|
597
595
|
} : routeDefs(), props.base || ""));
|
|
598
596
|
const routerState = createRouterContext(router, branches, {
|
|
599
|
-
base
|
|
600
|
-
actionBase
|
|
597
|
+
base
|
|
601
598
|
});
|
|
602
599
|
router.create && router.create(routerState);
|
|
603
600
|
return createComponent$1(RouterContextObj.Provider, {
|
|
@@ -768,6 +765,7 @@ if (!isServer) {
|
|
|
768
765
|
function getCache() {
|
|
769
766
|
if (!isServer) return cacheMap;
|
|
770
767
|
const req = getRequestEvent() || sharedConfig.context;
|
|
768
|
+
if (!req) throw new Error("Cannot find cache context");
|
|
771
769
|
return req.routerCache || (req.routerCache = new Map());
|
|
772
770
|
}
|
|
773
771
|
function revalidate(key) {
|
|
@@ -825,7 +823,8 @@ function cache(fn, name, options) {
|
|
|
825
823
|
|
|
826
824
|
// serialize on server
|
|
827
825
|
if (isServer && sharedConfig.context && !sharedConfig.context.noHydrate) {
|
|
828
|
-
|
|
826
|
+
const e = getRequestEvent();
|
|
827
|
+
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
829
828
|
}
|
|
830
829
|
if (cached) {
|
|
831
830
|
cached[0] = now;
|
|
@@ -868,6 +867,25 @@ function cache(fn, name, options) {
|
|
|
868
867
|
cachedFn.key = name;
|
|
869
868
|
return cachedFn;
|
|
870
869
|
}
|
|
870
|
+
cache.set = (key, value) => {
|
|
871
|
+
const cache = getCache();
|
|
872
|
+
const now = Date.now();
|
|
873
|
+
let cached = cache.get(key);
|
|
874
|
+
let version;
|
|
875
|
+
if (getOwner()) {
|
|
876
|
+
version = createSignal(now, {
|
|
877
|
+
equals: (p, v) => v - p < 50 // margin of error
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
onCleanup(() => cached[3].delete(version));
|
|
881
|
+
}
|
|
882
|
+
if (cached) {
|
|
883
|
+
cached[0] = now;
|
|
884
|
+
cached[1] = value;
|
|
885
|
+
cached[2] = "preload";
|
|
886
|
+
version && cached[3].add(version);
|
|
887
|
+
} else cache.set(key, cached = [now, value,, new Set(version ? [version] : [])]);
|
|
888
|
+
};
|
|
871
889
|
function matchKey(key, keys) {
|
|
872
890
|
for (let k of keys) {
|
|
873
891
|
if (key.startsWith(k)) return true;
|
|
@@ -991,98 +1009,105 @@ async function handleResponse(response, navigate) {
|
|
|
991
1009
|
return data;
|
|
992
1010
|
}
|
|
993
1011
|
|
|
994
|
-
function setupNativeEvents(
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
function handleAnchor(evt) {
|
|
1002
|
-
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
|
1003
|
-
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
1004
|
-
if (!a) return;
|
|
1005
|
-
const svg = isSvg(a);
|
|
1006
|
-
const href = svg ? a.href.baseVal : a.href;
|
|
1007
|
-
const target = svg ? a.target.baseVal : a.target;
|
|
1008
|
-
if (target || !href && !a.hasAttribute("state")) return;
|
|
1009
|
-
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
1010
|
-
if (a.hasAttribute("download") || rel && rel.includes("external")) return;
|
|
1011
|
-
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
1012
|
-
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
|
|
1013
|
-
return [a, url];
|
|
1014
|
-
}
|
|
1015
|
-
function handleAnchorClick(evt) {
|
|
1016
|
-
const res = handleAnchor(evt);
|
|
1017
|
-
if (!res) return;
|
|
1018
|
-
const [a, url] = res;
|
|
1019
|
-
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
1020
|
-
const state = a.getAttribute("state");
|
|
1021
|
-
evt.preventDefault();
|
|
1022
|
-
navigateFromRoute(to, {
|
|
1023
|
-
resolve: false,
|
|
1024
|
-
replace: a.hasAttribute("replace"),
|
|
1025
|
-
scroll: !a.hasAttribute("noscroll"),
|
|
1026
|
-
state: state && JSON.parse(state)
|
|
1027
|
-
});
|
|
1028
|
-
}
|
|
1029
|
-
function handleAnchorPreload(evt) {
|
|
1030
|
-
const res = handleAnchor(evt);
|
|
1031
|
-
if (!res) return;
|
|
1032
|
-
const [a, url] = res;
|
|
1033
|
-
if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1034
|
-
}
|
|
1035
|
-
function handleAnchorIn(evt) {
|
|
1036
|
-
const res = handleAnchor(evt);
|
|
1037
|
-
if (!res) return;
|
|
1038
|
-
const [a, url] = res;
|
|
1039
|
-
if (preloadTimeout[url.pathname]) return;
|
|
1040
|
-
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
1041
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1042
|
-
delete preloadTimeout[url.pathname];
|
|
1043
|
-
}, 200);
|
|
1044
|
-
}
|
|
1045
|
-
function handleAnchorOut(evt) {
|
|
1046
|
-
const res = handleAnchor(evt);
|
|
1047
|
-
if (!res) return;
|
|
1048
|
-
const [, url] = res;
|
|
1049
|
-
if (preloadTimeout[url.pathname]) {
|
|
1050
|
-
clearTimeout(preloadTimeout[url.pathname]);
|
|
1051
|
-
delete preloadTimeout[url.pathname];
|
|
1012
|
+
function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") {
|
|
1013
|
+
return router => {
|
|
1014
|
+
const basePath = router.base.path();
|
|
1015
|
+
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
1016
|
+
let preloadTimeout = {};
|
|
1017
|
+
function isSvg(el) {
|
|
1018
|
+
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
1052
1019
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
if (!
|
|
1020
|
+
function handleAnchor(evt) {
|
|
1021
|
+
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
|
1022
|
+
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
1023
|
+
if (!a || explicitLinks && !a.getAttribute("link")) return;
|
|
1024
|
+
const svg = isSvg(a);
|
|
1025
|
+
const href = svg ? a.href.baseVal : a.href;
|
|
1026
|
+
const target = svg ? a.target.baseVal : a.target;
|
|
1027
|
+
if (target || !href && !a.hasAttribute("state")) return;
|
|
1028
|
+
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
1029
|
+
if (a.hasAttribute("download") || rel && rel.includes("external")) return;
|
|
1030
|
+
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
1031
|
+
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
|
|
1032
|
+
return [a, url];
|
|
1061
1033
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1034
|
+
function handleAnchorClick(evt) {
|
|
1035
|
+
const res = handleAnchor(evt);
|
|
1036
|
+
if (!res) return;
|
|
1037
|
+
const [a, url] = res;
|
|
1038
|
+
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
1039
|
+
const state = a.getAttribute("state");
|
|
1064
1040
|
evt.preventDefault();
|
|
1065
|
-
|
|
1066
|
-
|
|
1041
|
+
navigateFromRoute(to, {
|
|
1042
|
+
resolve: false,
|
|
1043
|
+
replace: a.hasAttribute("replace"),
|
|
1044
|
+
scroll: !a.hasAttribute("noscroll"),
|
|
1045
|
+
state: state && JSON.parse(state)
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
function handleAnchorPreload(evt) {
|
|
1049
|
+
const res = handleAnchor(evt);
|
|
1050
|
+
if (!res) return;
|
|
1051
|
+
const [a, url] = res;
|
|
1052
|
+
if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1053
|
+
}
|
|
1054
|
+
function handleAnchorIn(evt) {
|
|
1055
|
+
const res = handleAnchor(evt);
|
|
1056
|
+
if (!res) return;
|
|
1057
|
+
const [a, url] = res;
|
|
1058
|
+
if (preloadTimeout[url.pathname]) return;
|
|
1059
|
+
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
1060
|
+
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1061
|
+
delete preloadTimeout[url.pathname];
|
|
1062
|
+
}, 200);
|
|
1063
|
+
}
|
|
1064
|
+
function handleAnchorOut(evt) {
|
|
1065
|
+
const res = handleAnchor(evt);
|
|
1066
|
+
if (!res) return;
|
|
1067
|
+
const [, url] = res;
|
|
1068
|
+
if (preloadTimeout[url.pathname]) {
|
|
1069
|
+
clearTimeout(preloadTimeout[url.pathname]);
|
|
1070
|
+
delete preloadTimeout[url.pathname];
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
function handleFormSubmit(evt) {
|
|
1074
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.formAction : evt.target.action;
|
|
1075
|
+
if (!actionRef) return;
|
|
1076
|
+
if (!actionRef.startsWith("action:")) {
|
|
1077
|
+
const url = new URL(actionRef);
|
|
1078
|
+
actionRef = router.parsePath(url.pathname + url.search);
|
|
1079
|
+
if (!actionRef.startsWith(actionBase)) return;
|
|
1080
|
+
}
|
|
1081
|
+
if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
|
|
1082
|
+
const handler = actions.get(actionRef);
|
|
1083
|
+
if (handler) {
|
|
1084
|
+
evt.preventDefault();
|
|
1085
|
+
const data = new FormData(evt.target);
|
|
1086
|
+
handler.call(router, data);
|
|
1087
|
+
}
|
|
1067
1088
|
}
|
|
1068
|
-
}
|
|
1069
1089
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
document.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1090
|
+
// ensure delegated event run first
|
|
1091
|
+
delegateEvents(["click", "submit"]);
|
|
1092
|
+
document.addEventListener("click", handleAnchorClick);
|
|
1093
|
+
if (preload) {
|
|
1094
|
+
document.addEventListener("mouseover", handleAnchorIn);
|
|
1095
|
+
document.addEventListener("mouseout", handleAnchorOut);
|
|
1096
|
+
document.addEventListener("focusin", handleAnchorPreload);
|
|
1097
|
+
document.addEventListener("touchstart", handleAnchorPreload);
|
|
1098
|
+
}
|
|
1099
|
+
document.addEventListener("submit", handleFormSubmit);
|
|
1100
|
+
onCleanup(() => {
|
|
1101
|
+
document.removeEventListener("click", handleAnchorClick);
|
|
1102
|
+
if (preload) {
|
|
1103
|
+
document.removeEventListener("mouseover", handleAnchorIn);
|
|
1104
|
+
document.removeEventListener("mouseout", handleAnchorOut);
|
|
1105
|
+
document.removeEventListener("focusin", handleAnchorPreload);
|
|
1106
|
+
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
1107
|
+
}
|
|
1108
|
+
document.removeEventListener("submit", handleFormSubmit);
|
|
1109
|
+
});
|
|
1110
|
+
};
|
|
1086
1111
|
}
|
|
1087
1112
|
|
|
1088
1113
|
function Router(props) {
|
|
@@ -1106,7 +1131,7 @@ function Router(props) {
|
|
|
1106
1131
|
scrollToHash(window.location.hash.slice(1), scroll);
|
|
1107
1132
|
},
|
|
1108
1133
|
init: notify => bindEvent(window, "popstate", () => notify()),
|
|
1109
|
-
create: setupNativeEvents,
|
|
1134
|
+
create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
|
|
1110
1135
|
utils: {
|
|
1111
1136
|
go: delta => window.history.go(delta)
|
|
1112
1137
|
}
|
|
@@ -1143,7 +1168,7 @@ function HashRouter(props) {
|
|
|
1143
1168
|
scrollToHash(hash, scroll);
|
|
1144
1169
|
},
|
|
1145
1170
|
init: notify => bindEvent(window, "hashchange", () => notify()),
|
|
1146
|
-
create: setupNativeEvents,
|
|
1171
|
+
create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
|
|
1147
1172
|
utils: {
|
|
1148
1173
|
go: delta => window.history.go(delta),
|
|
1149
1174
|
renderPath: path => `#${path}`,
|
|
@@ -1243,6 +1268,7 @@ function A(props) {
|
|
|
1243
1268
|
...rest.classList
|
|
1244
1269
|
};
|
|
1245
1270
|
},
|
|
1271
|
+
"link": "",
|
|
1246
1272
|
get ["aria-current"]() {
|
|
1247
1273
|
return isActive() ? "page" : undefined;
|
|
1248
1274
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { JSX } from "solid-js";
|
|
2
2
|
import type { BaseRouterProps } from "./components";
|
|
3
3
|
export declare function hashParser(str: string): string;
|
|
4
|
-
export type HashRouterProps = BaseRouterProps
|
|
4
|
+
export type HashRouterProps = BaseRouterProps & {
|
|
5
|
+
actionBase?: string;
|
|
6
|
+
explicitLinks?: boolean;
|
|
7
|
+
preload?: boolean;
|
|
8
|
+
};
|
|
5
9
|
export declare function HashRouter(props: HashRouterProps): JSX.Element;
|
|
@@ -26,7 +26,7 @@ export function HashRouter(props) {
|
|
|
26
26
|
scrollToHash(hash, scroll);
|
|
27
27
|
},
|
|
28
28
|
init: notify => bindEvent(window, "hashchange", () => notify()),
|
|
29
|
-
create: setupNativeEvents,
|
|
29
|
+
create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
|
|
30
30
|
utils: {
|
|
31
31
|
go: delta => window.history.go(delta),
|
|
32
32
|
renderPath: path => `#${path}`,
|
package/dist/routers/Router.d.ts
CHANGED
|
@@ -2,5 +2,8 @@ import type { BaseRouterProps } from "./components";
|
|
|
2
2
|
import type { JSX } from "solid-js";
|
|
3
3
|
export type RouterProps = BaseRouterProps & {
|
|
4
4
|
url?: string;
|
|
5
|
+
actionBase?: string;
|
|
6
|
+
explicitLinks?: boolean;
|
|
7
|
+
preload?: boolean;
|
|
5
8
|
};
|
|
6
9
|
export declare function Router(props: RouterProps): JSX.Element;
|
package/dist/routers/Router.js
CHANGED
|
@@ -20,7 +20,7 @@ export function Router(props) {
|
|
|
20
20
|
scrollToHash(window.location.hash.slice(1), scroll);
|
|
21
21
|
},
|
|
22
22
|
init: notify => bindEvent(window, "popstate", () => notify()),
|
|
23
|
-
create: setupNativeEvents,
|
|
23
|
+
create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
|
|
24
24
|
utils: {
|
|
25
25
|
go: delta => window.history.go(delta)
|
|
26
26
|
}
|
|
@@ -2,7 +2,6 @@ import type { Component, JSX } from "solid-js";
|
|
|
2
2
|
import type { MatchFilters, RouteLoadFunc, RouterIntegration, RouteSectionProps } from "../types";
|
|
3
3
|
export type BaseRouterProps = {
|
|
4
4
|
base?: string;
|
|
5
|
-
actionBase?: string;
|
|
6
5
|
root?: Component<RouteSectionProps>;
|
|
7
6
|
children?: JSX.Element;
|
|
8
7
|
};
|
|
@@ -3,10 +3,10 @@ import { children, createMemo, createRoot, mergeProps, on, Show } from "solid-js
|
|
|
3
3
|
import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj } from "../routing";
|
|
4
4
|
import { createMemoObject } from "../utils";
|
|
5
5
|
export const createRouterComponent = (router) => (props) => {
|
|
6
|
-
const { base
|
|
6
|
+
const { base } = props;
|
|
7
7
|
const routeDefs = children(() => props.children);
|
|
8
8
|
const branches = createMemo(() => createBranches(props.root ? { component: props.root, children: routeDefs() } : routeDefs(), props.base || ""));
|
|
9
|
-
const routerState = createRouterContext(router, branches, { base
|
|
9
|
+
const routerState = createRouterContext(router, branches, { base });
|
|
10
10
|
router.create && router.create(routerState);
|
|
11
11
|
return (<RouterContextObj.Provider value={routerState}>
|
|
12
12
|
<Routes routerState={routerState} branches={branches()}/>
|
package/dist/routing.d.ts
CHANGED
|
@@ -21,6 +21,5 @@ export declare function createLocation(path: Accessor<string>, state: Accessor<a
|
|
|
21
21
|
export declare function getIntent(): Intent | undefined;
|
|
22
22
|
export declare function createRouterContext(integration: RouterIntegration, getBranches?: () => Branch[], options?: {
|
|
23
23
|
base?: string;
|
|
24
|
-
actionBase?: string;
|
|
25
24
|
}): RouterContext;
|
|
26
25
|
export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, params: Params): RouteContext;
|
package/dist/routing.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ declare module "solid-js/web" {
|
|
|
4
4
|
response?: Response;
|
|
5
5
|
routerCache?: Map<any, any>;
|
|
6
6
|
initialSubmission?: Submission<any, any>;
|
|
7
|
+
serverOnly?: boolean;
|
|
7
8
|
}
|
|
8
9
|
}
|
|
9
10
|
export type Params = Record<string, string>;
|
|
@@ -118,7 +119,6 @@ export interface RouterOutput {
|
|
|
118
119
|
}
|
|
119
120
|
export interface RouterContext {
|
|
120
121
|
base: RouteContext;
|
|
121
|
-
actionBase: string;
|
|
122
122
|
location: Location;
|
|
123
123
|
navigatorFactory: NavigatorFactory;
|
|
124
124
|
isRouting: () => boolean;
|