@solidjs/router 0.10.0-beta.8 → 0.10.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 +97 -10
- package/dist/components.d.ts +1 -0
- package/dist/components.jsx +1 -1
- package/dist/data/action.d.ts +1 -1
- package/dist/data/action.js +11 -5
- package/dist/data/cache.d.ts +8 -1
- package/dist/data/cache.js +36 -10
- package/dist/data/events.d.ts +1 -1
- package/dist/data/events.js +117 -109
- package/dist/index.js +139 -107
- 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
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
# Solid Router [](https://www.npmjs.org/package/@solidjs/router)
|
|
6
6
|
|
|
7
|
+
**Version 0.10.0 requires Solid v1.8.4 or later.**
|
|
8
|
+
|
|
7
9
|
A router lets you change your view based on the URL in the browser. This allows your "single-page" application to simulate a traditional multipage site. To use Solid Router, you specify components called Routes that depend on the value of the URL (the "path"), and the router handles the mechanism of swapping them in and out.
|
|
8
10
|
|
|
9
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.
|
|
@@ -368,7 +370,17 @@ This cache accomplishes the following:
|
|
|
368
370
|
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
371
|
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
372
|
|
|
371
|
-
|
|
373
|
+
Cached function has a few useful methods for getting the key that are useful for invalidation.
|
|
374
|
+
```ts
|
|
375
|
+
let id = 5;
|
|
376
|
+
|
|
377
|
+
getUser.key // returns "users"
|
|
378
|
+
getUser.keyFor(id) // returns "users[5]"
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
You can revalidate the cache using the `revalidate` method or you can set `revalidate` keys on your response from your actions. If you pass the whole key it will invalidate all the entries for the cache (ie "users" in the example above). You can also invalidate a single entry by using `keyFor`.
|
|
382
|
+
|
|
383
|
+
`cache` can be defined anywhere and then used inside your components with:
|
|
372
384
|
|
|
373
385
|
### `createAsync`
|
|
374
386
|
|
|
@@ -378,9 +390,11 @@ This is light wrapper over `createResource` that aims to serve as stand-in for a
|
|
|
378
390
|
const user = createAsync(() => getUser(params.id))
|
|
379
391
|
```
|
|
380
392
|
|
|
393
|
+
Using `cache` in `createResource` directly won't work properly as the fetcher is not reactive and it won't invalidate properly.
|
|
394
|
+
|
|
381
395
|
### `action`
|
|
382
396
|
|
|
383
|
-
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response
|
|
397
|
+
Actions are data mutations that can trigger invalidations and further routing. A list of prebuilt response helpers can be found below.
|
|
384
398
|
```jsx
|
|
385
399
|
import { action, revalidate, redirect } from "@solidjs/router"
|
|
386
400
|
|
|
@@ -391,12 +405,38 @@ const myAction = action(async (data) => {
|
|
|
391
405
|
});
|
|
392
406
|
|
|
393
407
|
// in component
|
|
394
|
-
<form action={myAction} />
|
|
408
|
+
<form action={myAction} method="post" />
|
|
395
409
|
|
|
396
410
|
//or
|
|
397
411
|
<button type="submit" formaction={myAction}></button>
|
|
398
412
|
```
|
|
399
413
|
|
|
414
|
+
Actions only work with post requests, so make sure to put `method="post"` on your form.
|
|
415
|
+
|
|
416
|
+
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.
|
|
417
|
+
|
|
418
|
+
Picture an action that deletes Todo Item:
|
|
419
|
+
|
|
420
|
+
```js
|
|
421
|
+
const deleteTodo = action(async (formData: FormData) => {
|
|
422
|
+
const id = Number(formData.get("id"))
|
|
423
|
+
await api.deleteTodo(id)
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
<form action={deleteUser} method="post">
|
|
427
|
+
<input type="hidden" name="id" value={todo.id} />
|
|
428
|
+
<button type="submit">Delete</button>
|
|
429
|
+
</form>
|
|
430
|
+
```
|
|
431
|
+
Instead with `with` you can write this:
|
|
432
|
+
```js
|
|
433
|
+
const deleteUser = action(api.deleteUser)
|
|
434
|
+
|
|
435
|
+
<form action={deleteUser.with(todo.id)} method="post">
|
|
436
|
+
<button type="submit">Delete</button>
|
|
437
|
+
</form>
|
|
438
|
+
```
|
|
439
|
+
|
|
400
440
|
#### Notes of `<form>` implementation and SSR
|
|
401
441
|
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
442
|
|
|
@@ -414,7 +454,7 @@ const submit = useAction(myAction)
|
|
|
414
454
|
submit(...args)
|
|
415
455
|
```
|
|
416
456
|
|
|
417
|
-
The outside of a form context you can use custom data instead of formData, and these helpers preserve types.
|
|
457
|
+
The outside of a form context you can use custom data instead of formData, and these helpers preserve types. However, even when used with server functions (in projects like SolidStart) this requires client side javascript and is not Progressive Enhancible like forms are.
|
|
418
458
|
|
|
419
459
|
### `useSubmission`/`useSubmissions`
|
|
420
460
|
|
|
@@ -434,6 +474,36 @@ const submissions = useSubmissions(action, (input) => filter(input));
|
|
|
434
474
|
const submission = useSubmission(action, (input) => filter(input));
|
|
435
475
|
```
|
|
436
476
|
|
|
477
|
+
### Response Helpers
|
|
478
|
+
|
|
479
|
+
These are used to communicate router navigations from cache/actions, and can include invalidation hints. Generally these are thrown to not interfere the with the types and make it clear that function ends execution at that point.
|
|
480
|
+
|
|
481
|
+
#### `redirect(path, options)`
|
|
482
|
+
|
|
483
|
+
Redirects to the next route
|
|
484
|
+
```js
|
|
485
|
+
const getUser = cache(() => {
|
|
486
|
+
const user = await api.getCurrentUser()
|
|
487
|
+
if (!user) throw redirect("/login");
|
|
488
|
+
return user;
|
|
489
|
+
})
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
#### `reload(options)`
|
|
493
|
+
|
|
494
|
+
Reloads the data on the current page
|
|
495
|
+
```js
|
|
496
|
+
const getTodo = cache(async (id: number) => {
|
|
497
|
+
const todo = await fetchTodo(id);
|
|
498
|
+
return todo;
|
|
499
|
+
}, "todo")
|
|
500
|
+
|
|
501
|
+
const updateTodo = action(async (todo: Todo) => {
|
|
502
|
+
await updateTodo(todo.id, todo);
|
|
503
|
+
reload({ revalidate: getTodo.keyFor(id) })
|
|
504
|
+
})
|
|
505
|
+
```
|
|
506
|
+
|
|
437
507
|
### Load Functions
|
|
438
508
|
|
|
439
509
|
Even with the cache API it is possible that we have waterfalls both with view logic and with lazy loaded code. With load functions, we can instead start fetching the data parallel to loading the route, so we can use the data as soon as possible.
|
|
@@ -571,9 +641,22 @@ import { Router } from "@solidjs/router";
|
|
|
571
641
|
|
|
572
642
|
## Components
|
|
573
643
|
|
|
644
|
+
### `<Router>`
|
|
645
|
+
|
|
646
|
+
This is the main Router component for the browser.
|
|
647
|
+
|
|
648
|
+
| prop | type | description |
|
|
649
|
+
|-----|----|----|
|
|
650
|
+
| children | `JSX.Element` or `RouteDefinition[]` | The route definitions |
|
|
651
|
+
| root | Component | Top level layout component |
|
|
652
|
+
| base | string | Base url to use for matching routes |
|
|
653
|
+
| actionBase | string | Root url for server actions, default: `/_server` |
|
|
654
|
+
| preload | boolean | Enables/disables preloads globally, default: `true` |
|
|
655
|
+
| explicitLinks | boolean | Disables all anchors being intercepted and instead requires `<A>`. default: `false` |
|
|
656
|
+
|
|
574
657
|
### `<A>`
|
|
575
658
|
|
|
576
|
-
Like the `<a>` tag but supports relative paths and active class styling.
|
|
659
|
+
Like the `<a>` tag but supports relative paths and active class styling (requires client side JavaScript).
|
|
577
660
|
|
|
578
661
|
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
662
|
|
|
@@ -608,7 +691,11 @@ The Component for defining Routes:
|
|
|
608
691
|
|
|
609
692
|
| prop | type | description |
|
|
610
693
|
|-|-|-|
|
|
611
|
-
|
|
|
694
|
+
| path | string | Path partial for defining the route segment |
|
|
695
|
+
| component | `Component` | Component that will be rendered for the matched segment |
|
|
696
|
+
| matchFilters | `MatchFilters` | Additional constraints for matching against the route |
|
|
697
|
+
| children | `JSX.Element` | Nested `<Route>` definitions |
|
|
698
|
+
| load | `RouteLoadFunc` | Function called during preload or when the route is navigated to. |
|
|
612
699
|
|
|
613
700
|
## Router Primitives
|
|
614
701
|
|
|
@@ -729,15 +816,15 @@ useBeforeLeave((e: BeforeLeaveEventArgs) => {
|
|
|
729
816
|
});
|
|
730
817
|
```
|
|
731
818
|
|
|
732
|
-
## Migrations from 0.
|
|
819
|
+
## Migrations from 0.9.x
|
|
733
820
|
|
|
734
|
-
v0.
|
|
821
|
+
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
822
|
|
|
736
823
|
The biggest changes are around removed APIs that need to be replaced.
|
|
737
824
|
|
|
738
825
|
### `<Outlet>`, `<Routes>`, `useRoutes`
|
|
739
826
|
|
|
740
|
-
This is no longer used and instead will use `props.children` passed from into the page components for outlets. Nested Routes inherently cause waterfalls and are Outlets
|
|
827
|
+
This is no longer used and instead will use `props.children` passed from into the page components for outlets. This keeps the outlet directly passed from its page and avoids oddness of trying to use context across Islands boundaries. Nested `<Routes>` components inherently cause waterfalls and are `<Outlets>` themselves so they have the same concerns. We do not want to encourage the pattern and if you must do it you can always nest `<Router>`s with appropriate base path.
|
|
741
828
|
|
|
742
829
|
## `element` prop removed from `Route`
|
|
743
830
|
|
|
@@ -745,7 +832,7 @@ Related without Outlet component it has to be passed in manually. At which point
|
|
|
745
832
|
|
|
746
833
|
### `data` functions & `useRouteData`
|
|
747
834
|
|
|
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.
|
|
835
|
+
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
836
|
|
|
750
837
|
## SPAs in Deployed Environments
|
|
751
838
|
|
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/action.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
|
|
1
|
+
import { $TRACK, createMemo, createSignal, onCleanup, getOwner, } from "solid-js";
|
|
2
2
|
import { isServer } from "solid-js/web";
|
|
3
3
|
import { useRouter } from "../routing";
|
|
4
4
|
import { redirectStatusCodes } from "../utils";
|
|
5
|
-
import { hashKey, revalidate } from "./cache";
|
|
5
|
+
import { cacheKeyOp, hashKey, revalidate } from "./cache";
|
|
6
6
|
export const actions = /* #__PURE__ */ new Map();
|
|
7
7
|
export function useSubmissions(fn, filter) {
|
|
8
8
|
const router = useRouter();
|
|
@@ -65,7 +65,9 @@ export function action(fn, name) {
|
|
|
65
65
|
p.then(handler, handler);
|
|
66
66
|
return p;
|
|
67
67
|
}
|
|
68
|
-
const url = fn.url ||
|
|
68
|
+
const url = fn.url ||
|
|
69
|
+
(name && `action:${name}`) ||
|
|
70
|
+
(!isServer ? `action:${hashString(fn.toString())}` : "");
|
|
69
71
|
return toAction(mutate, url);
|
|
70
72
|
}
|
|
71
73
|
function toAction(fn, url) {
|
|
@@ -80,7 +82,7 @@ function toAction(fn, url) {
|
|
|
80
82
|
};
|
|
81
83
|
const uri = new URL(url, "http://sar");
|
|
82
84
|
uri.searchParams.set("args", hashKey(args));
|
|
83
|
-
return toAction(newFn, uri.pathname + uri.search);
|
|
85
|
+
return toAction(newFn, (uri.protocol === "action:" ? uri.protocol : "") + uri.pathname + uri.search);
|
|
84
86
|
};
|
|
85
87
|
fn.url = url;
|
|
86
88
|
if (!isServer) {
|
|
@@ -89,12 +91,15 @@ function toAction(fn, url) {
|
|
|
89
91
|
}
|
|
90
92
|
return fn;
|
|
91
93
|
}
|
|
94
|
+
const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
|
|
92
95
|
async function handleResponse(response, navigate) {
|
|
93
96
|
let data;
|
|
94
97
|
let keys;
|
|
95
98
|
if (response instanceof Response) {
|
|
96
99
|
if (response.headers.has("X-Revalidate")) {
|
|
97
100
|
keys = response.headers.get("X-Revalidate").split(",");
|
|
101
|
+
// invalidate
|
|
102
|
+
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
98
103
|
}
|
|
99
104
|
if (response.customBody)
|
|
100
105
|
data = await response.customBody();
|
|
@@ -110,6 +115,7 @@ async function handleResponse(response, navigate) {
|
|
|
110
115
|
}
|
|
111
116
|
else
|
|
112
117
|
data = response;
|
|
113
|
-
|
|
118
|
+
// trigger revalidation
|
|
119
|
+
await revalidate(keys, false);
|
|
114
120
|
return data;
|
|
115
121
|
}
|
package/dist/data/cache.d.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
+
import { type Signal } from "solid-js";
|
|
1
2
|
import { type ReconcileOptions } from "solid-js/store";
|
|
2
|
-
|
|
3
|
+
type CacheEntry = [number, any, string, Set<Signal<number>>];
|
|
4
|
+
export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
|
|
5
|
+
export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
|
|
3
6
|
export type CachedFunction<T extends (...args: any) => U | Response, U> = T & {
|
|
4
7
|
keyFor: (...args: Parameters<T>) => string;
|
|
5
8
|
key: string;
|
|
6
9
|
};
|
|
7
10
|
export declare function cache<T extends (...args: any) => U | Response, U>(fn: T, name: string, options?: ReconcileOptions): CachedFunction<T, U>;
|
|
11
|
+
export declare namespace cache {
|
|
12
|
+
var set: (key: string, value: any) => void;
|
|
13
|
+
}
|
|
8
14
|
export declare function hashKey<T extends Array<any>>(args: T): string;
|
|
15
|
+
export {};
|
package/dist/data/cache.js
CHANGED
|
@@ -22,21 +22,26 @@ 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
|
-
export function revalidate(key) {
|
|
28
|
-
key && !Array.isArray(key) && (key = [key]);
|
|
29
|
+
export function revalidate(key, force = true) {
|
|
29
30
|
return startTransition(() => {
|
|
30
31
|
const now = Date.now();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
revalidateSignals(entry[3], now); // retrigger live signals
|
|
36
|
-
}
|
|
37
|
-
}
|
|
32
|
+
cacheKeyOp(key, entry => {
|
|
33
|
+
force && (entry[0] = 0); //force cache miss
|
|
34
|
+
revalidateSignals(entry[3], now); // retrigger live signals
|
|
35
|
+
});
|
|
38
36
|
});
|
|
39
37
|
}
|
|
38
|
+
export function cacheKeyOp(key, fn) {
|
|
39
|
+
key && !Array.isArray(key) && (key = [key]);
|
|
40
|
+
for (let k of cacheMap.keys()) {
|
|
41
|
+
if (key === undefined || matchKey(k, key))
|
|
42
|
+
fn(cacheMap.get(k));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
40
45
|
function revalidateSignals(set, time) {
|
|
41
46
|
for (let s of set)
|
|
42
47
|
s[1](time);
|
|
@@ -79,7 +84,8 @@ export function cache(fn, name, options) {
|
|
|
79
84
|
: fn(...args);
|
|
80
85
|
// serialize on server
|
|
81
86
|
if (isServer && sharedConfig.context && !sharedConfig.context.noHydrate) {
|
|
82
|
-
|
|
87
|
+
const e = getRequestEvent();
|
|
88
|
+
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
83
89
|
}
|
|
84
90
|
if (cached) {
|
|
85
91
|
cached[0] = now;
|
|
@@ -130,6 +136,26 @@ export function cache(fn, name, options) {
|
|
|
130
136
|
cachedFn.key = name;
|
|
131
137
|
return cachedFn;
|
|
132
138
|
}
|
|
139
|
+
cache.set = (key, value) => {
|
|
140
|
+
const cache = getCache();
|
|
141
|
+
const now = Date.now();
|
|
142
|
+
let cached = cache.get(key);
|
|
143
|
+
let version;
|
|
144
|
+
if (getOwner()) {
|
|
145
|
+
version = createSignal(now, {
|
|
146
|
+
equals: (p, v) => v - p < 50 // margin of error
|
|
147
|
+
});
|
|
148
|
+
onCleanup(() => cached[3].delete(version));
|
|
149
|
+
}
|
|
150
|
+
if (cached) {
|
|
151
|
+
cached[0] = now;
|
|
152
|
+
cached[1] = value;
|
|
153
|
+
cached[2] = "preload";
|
|
154
|
+
version && cached[3].add(version);
|
|
155
|
+
}
|
|
156
|
+
else
|
|
157
|
+
cache.set(key, (cached = [now, value, , new Set(version ? [version] : [])]));
|
|
158
|
+
};
|
|
133
159
|
function matchKey(key, keys) {
|
|
134
160
|
for (let k of keys) {
|
|
135
161
|
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,22 +765,25 @@ 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
|
-
function revalidate(key) {
|
|
774
|
-
key && !Array.isArray(key) && (key = [key]);
|
|
771
|
+
function revalidate(key, force = true) {
|
|
775
772
|
return startTransition(() => {
|
|
776
773
|
const now = Date.now();
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
revalidateSignals(entry[3], now); // retrigger live signals
|
|
782
|
-
}
|
|
783
|
-
}
|
|
774
|
+
cacheKeyOp(key, entry => {
|
|
775
|
+
force && (entry[0] = 0); //force cache miss
|
|
776
|
+
revalidateSignals(entry[3], now); // retrigger live signals
|
|
777
|
+
});
|
|
784
778
|
});
|
|
785
779
|
}
|
|
786
780
|
|
|
781
|
+
function cacheKeyOp(key, fn) {
|
|
782
|
+
key && !Array.isArray(key) && (key = [key]);
|
|
783
|
+
for (let k of cacheMap.keys()) {
|
|
784
|
+
if (key === undefined || matchKey(k, key)) fn(cacheMap.get(k));
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
787
|
function revalidateSignals(set, time) {
|
|
788
788
|
for (let s of set) s[1](time);
|
|
789
789
|
}
|
|
@@ -825,7 +825,8 @@ function cache(fn, name, options) {
|
|
|
825
825
|
|
|
826
826
|
// serialize on server
|
|
827
827
|
if (isServer && sharedConfig.context && !sharedConfig.context.noHydrate) {
|
|
828
|
-
|
|
828
|
+
const e = getRequestEvent();
|
|
829
|
+
(!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
|
|
829
830
|
}
|
|
830
831
|
if (cached) {
|
|
831
832
|
cached[0] = now;
|
|
@@ -868,6 +869,25 @@ function cache(fn, name, options) {
|
|
|
868
869
|
cachedFn.key = name;
|
|
869
870
|
return cachedFn;
|
|
870
871
|
}
|
|
872
|
+
cache.set = (key, value) => {
|
|
873
|
+
const cache = getCache();
|
|
874
|
+
const now = Date.now();
|
|
875
|
+
let cached = cache.get(key);
|
|
876
|
+
let version;
|
|
877
|
+
if (getOwner()) {
|
|
878
|
+
version = createSignal(now, {
|
|
879
|
+
equals: (p, v) => v - p < 50 // margin of error
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
onCleanup(() => cached[3].delete(version));
|
|
883
|
+
}
|
|
884
|
+
if (cached) {
|
|
885
|
+
cached[0] = now;
|
|
886
|
+
cached[1] = value;
|
|
887
|
+
cached[2] = "preload";
|
|
888
|
+
version && cached[3].add(version);
|
|
889
|
+
} else cache.set(key, cached = [now, value,, new Set(version ? [version] : [])]);
|
|
890
|
+
};
|
|
871
891
|
function matchKey(key, keys) {
|
|
872
892
|
for (let k of keys) {
|
|
873
893
|
if (key.startsWith(k)) return true;
|
|
@@ -947,7 +967,7 @@ function action(fn, name) {
|
|
|
947
967
|
p.then(handler, handler);
|
|
948
968
|
return p;
|
|
949
969
|
}
|
|
950
|
-
const url = fn.url || name && `action:${name}` || (!isServer ? `action:${fn.
|
|
970
|
+
const url = fn.url || name && `action:${name}` || (!isServer ? `action:${hashString(fn.toString())}` : "");
|
|
951
971
|
return toAction(mutate, url);
|
|
952
972
|
}
|
|
953
973
|
function toAction(fn, url) {
|
|
@@ -961,7 +981,7 @@ function toAction(fn, url) {
|
|
|
961
981
|
};
|
|
962
982
|
const uri = new URL(url, "http://sar");
|
|
963
983
|
uri.searchParams.set("args", hashKey(args));
|
|
964
|
-
return toAction(newFn, uri.pathname + uri.search);
|
|
984
|
+
return toAction(newFn, (uri.protocol === "action:" ? uri.protocol : "") + uri.pathname + uri.search);
|
|
965
985
|
};
|
|
966
986
|
fn.url = url;
|
|
967
987
|
if (!isServer) {
|
|
@@ -970,12 +990,15 @@ function toAction(fn, url) {
|
|
|
970
990
|
}
|
|
971
991
|
return fn;
|
|
972
992
|
}
|
|
993
|
+
const hashString = s => s.split("").reduce((a, b) => (a << 5) - a + b.charCodeAt(0) | 0, 0);
|
|
973
994
|
async function handleResponse(response, navigate) {
|
|
974
995
|
let data;
|
|
975
996
|
let keys;
|
|
976
997
|
if (response instanceof Response) {
|
|
977
998
|
if (response.headers.has("X-Revalidate")) {
|
|
978
999
|
keys = response.headers.get("X-Revalidate").split(",");
|
|
1000
|
+
// invalidate
|
|
1001
|
+
cacheKeyOp(keys, entry => entry[0] = 0);
|
|
979
1002
|
}
|
|
980
1003
|
if (response.customBody) data = await response.customBody();
|
|
981
1004
|
if (redirectStatusCodes.has(response.status)) {
|
|
@@ -987,102 +1010,110 @@ async function handleResponse(response, navigate) {
|
|
|
987
1010
|
}
|
|
988
1011
|
}
|
|
989
1012
|
} else data = response;
|
|
990
|
-
|
|
1013
|
+
// trigger revalidation
|
|
1014
|
+
await revalidate(keys, false);
|
|
991
1015
|
return data;
|
|
992
1016
|
}
|
|
993
1017
|
|
|
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];
|
|
1018
|
+
function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") {
|
|
1019
|
+
return router => {
|
|
1020
|
+
const basePath = router.base.path();
|
|
1021
|
+
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
1022
|
+
let preloadTimeout = {};
|
|
1023
|
+
function isSvg(el) {
|
|
1024
|
+
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
1052
1025
|
}
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
if (!
|
|
1026
|
+
function handleAnchor(evt) {
|
|
1027
|
+
if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
|
|
1028
|
+
const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
1029
|
+
if (!a || explicitLinks && !a.getAttribute("link")) return;
|
|
1030
|
+
const svg = isSvg(a);
|
|
1031
|
+
const href = svg ? a.href.baseVal : a.href;
|
|
1032
|
+
const target = svg ? a.target.baseVal : a.target;
|
|
1033
|
+
if (target || !href && !a.hasAttribute("state")) return;
|
|
1034
|
+
const rel = (a.getAttribute("rel") || "").split(/\s+/);
|
|
1035
|
+
if (a.hasAttribute("download") || rel && rel.includes("external")) return;
|
|
1036
|
+
const url = svg ? new URL(href, document.baseURI) : new URL(href);
|
|
1037
|
+
if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
|
|
1038
|
+
return [a, url];
|
|
1061
1039
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1040
|
+
function handleAnchorClick(evt) {
|
|
1041
|
+
const res = handleAnchor(evt);
|
|
1042
|
+
if (!res) return;
|
|
1043
|
+
const [a, url] = res;
|
|
1044
|
+
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
1045
|
+
const state = a.getAttribute("state");
|
|
1064
1046
|
evt.preventDefault();
|
|
1065
|
-
|
|
1066
|
-
|
|
1047
|
+
navigateFromRoute(to, {
|
|
1048
|
+
resolve: false,
|
|
1049
|
+
replace: a.hasAttribute("replace"),
|
|
1050
|
+
scroll: !a.hasAttribute("noscroll"),
|
|
1051
|
+
state: state && JSON.parse(state)
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
function handleAnchorPreload(evt) {
|
|
1055
|
+
const res = handleAnchor(evt);
|
|
1056
|
+
if (!res) return;
|
|
1057
|
+
const [a, url] = res;
|
|
1058
|
+
if (!preloadTimeout[url.pathname]) router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1059
|
+
}
|
|
1060
|
+
function handleAnchorIn(evt) {
|
|
1061
|
+
const res = handleAnchor(evt);
|
|
1062
|
+
if (!res) return;
|
|
1063
|
+
const [a, url] = res;
|
|
1064
|
+
if (preloadTimeout[url.pathname]) return;
|
|
1065
|
+
preloadTimeout[url.pathname] = setTimeout(() => {
|
|
1066
|
+
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
1067
|
+
delete preloadTimeout[url.pathname];
|
|
1068
|
+
}, 200);
|
|
1069
|
+
}
|
|
1070
|
+
function handleAnchorOut(evt) {
|
|
1071
|
+
const res = handleAnchor(evt);
|
|
1072
|
+
if (!res) return;
|
|
1073
|
+
const [, url] = res;
|
|
1074
|
+
if (preloadTimeout[url.pathname]) {
|
|
1075
|
+
clearTimeout(preloadTimeout[url.pathname]);
|
|
1076
|
+
delete preloadTimeout[url.pathname];
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
function handleFormSubmit(evt) {
|
|
1080
|
+
let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.formAction : evt.target.action;
|
|
1081
|
+
if (!actionRef) return;
|
|
1082
|
+
if (!actionRef.startsWith("action:")) {
|
|
1083
|
+
const url = new URL(actionRef);
|
|
1084
|
+
actionRef = router.parsePath(url.pathname + url.search);
|
|
1085
|
+
if (!actionRef.startsWith(actionBase)) return;
|
|
1086
|
+
}
|
|
1087
|
+
if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
|
|
1088
|
+
const handler = actions.get(actionRef);
|
|
1089
|
+
if (handler) {
|
|
1090
|
+
evt.preventDefault();
|
|
1091
|
+
const data = new FormData(evt.target);
|
|
1092
|
+
handler.call(router, data);
|
|
1093
|
+
}
|
|
1067
1094
|
}
|
|
1068
|
-
}
|
|
1069
1095
|
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
document.
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1096
|
+
// ensure delegated event run first
|
|
1097
|
+
delegateEvents(["click", "submit"]);
|
|
1098
|
+
document.addEventListener("click", handleAnchorClick);
|
|
1099
|
+
if (preload) {
|
|
1100
|
+
document.addEventListener("mouseover", handleAnchorIn);
|
|
1101
|
+
document.addEventListener("mouseout", handleAnchorOut);
|
|
1102
|
+
document.addEventListener("focusin", handleAnchorPreload);
|
|
1103
|
+
document.addEventListener("touchstart", handleAnchorPreload);
|
|
1104
|
+
}
|
|
1105
|
+
document.addEventListener("submit", handleFormSubmit);
|
|
1106
|
+
onCleanup(() => {
|
|
1107
|
+
document.removeEventListener("click", handleAnchorClick);
|
|
1108
|
+
if (preload) {
|
|
1109
|
+
document.removeEventListener("mouseover", handleAnchorIn);
|
|
1110
|
+
document.removeEventListener("mouseout", handleAnchorOut);
|
|
1111
|
+
document.removeEventListener("focusin", handleAnchorPreload);
|
|
1112
|
+
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
1113
|
+
}
|
|
1114
|
+
document.removeEventListener("submit", handleFormSubmit);
|
|
1115
|
+
});
|
|
1116
|
+
};
|
|
1086
1117
|
}
|
|
1087
1118
|
|
|
1088
1119
|
function Router(props) {
|
|
@@ -1106,7 +1137,7 @@ function Router(props) {
|
|
|
1106
1137
|
scrollToHash(window.location.hash.slice(1), scroll);
|
|
1107
1138
|
},
|
|
1108
1139
|
init: notify => bindEvent(window, "popstate", () => notify()),
|
|
1109
|
-
create: setupNativeEvents,
|
|
1140
|
+
create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
|
|
1110
1141
|
utils: {
|
|
1111
1142
|
go: delta => window.history.go(delta)
|
|
1112
1143
|
}
|
|
@@ -1143,7 +1174,7 @@ function HashRouter(props) {
|
|
|
1143
1174
|
scrollToHash(hash, scroll);
|
|
1144
1175
|
},
|
|
1145
1176
|
init: notify => bindEvent(window, "hashchange", () => notify()),
|
|
1146
|
-
create: setupNativeEvents,
|
|
1177
|
+
create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
|
|
1147
1178
|
utils: {
|
|
1148
1179
|
go: delta => window.history.go(delta),
|
|
1149
1180
|
renderPath: path => `#${path}`,
|
|
@@ -1243,6 +1274,7 @@ function A(props) {
|
|
|
1243
1274
|
...rest.classList
|
|
1244
1275
|
};
|
|
1245
1276
|
},
|
|
1277
|
+
"link": "",
|
|
1246
1278
|
get ["aria-current"]() {
|
|
1247
1279
|
return isActive() ? "page" : undefined;
|
|
1248
1280
|
}
|
|
@@ -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;
|