@solidjs/router 2.0.0-next.36 → 2.0.0-next.37
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 +30 -7
- package/dist/claims.d.ts +5 -3
- package/dist/claims.js +14 -14
- package/dist/data/events.js +9 -5
- package/dist/index.d.ts +2 -1
- package/dist/index.js +231 -109
- package/dist/index.jsx +1 -0
- package/dist/pending.d.ts +25 -0
- package/dist/pending.js +67 -0
- package/dist/routers/components.jsx +7 -4
- package/dist/routers/factory.d.ts +17 -2
- package/dist/routers/factory.jsx +22 -13
- package/dist/routers/scrollRestoration.d.ts +29 -5
- package/dist/routers/scrollRestoration.js +77 -32
- package/dist/routing.d.ts +7 -1
- package/dist/routing.js +28 -38
- package/dist/serverRouteComponent.js +5 -4
- package/dist/types.d.ts +27 -10
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
|
|
|
17
17
|
## Core Features
|
|
18
18
|
|
|
19
19
|
- **Typed Routing**: URLs built through a typed path proxy inferred from your route config — `paths.users(2).settings` typechecks against the tree
|
|
20
|
-
- **Plain Anchors**: no link component — `<a>` elements get `aria-current
|
|
20
|
+
- **Plain Anchors**: no link component — `<a>` elements get `aria-current` and `data-active` automatically via compiler-claimed anchors, and `data-pending` with the opt-in `pendingLinks` plugin
|
|
21
21
|
- **Universal Rendering**: one factory for browser, hash, memory, and server rendering; history adapters are imports, so unused ones never enter your bundle
|
|
22
22
|
- **Preload Functions**: parallel data fetching following the render-as-you-fetch pattern, triggered eagerly on link hover/focus
|
|
23
23
|
- **Data APIs with Caching**: `query` and `action` with deduplication, revalidation, single-flight mutations, and progressive enhancement — plus experimental `liveQuery` for keyed queries over live streams
|
|
@@ -425,7 +425,7 @@ const routes = [{ component: serverRouteComponent(query(appShell, "shell")), chi
|
|
|
425
425
|
render(() => <Router routes={routes} />, document.body);
|
|
426
426
|
```
|
|
427
427
|
|
|
428
|
-
`children` is the only client position the router fills, and the helper's type says so: a server component that requires other props — event handlers, refs, named slots — is rejected. Those come from the client, so that route has a client half; write it as an ordinary route component around `
|
|
428
|
+
`children` is the only client position the router fills, and the helper's type says so: a server component that requires other props — event handlers, refs, named slots — is rejected. Those come from the client, so that route has a client half; write it as an ordinary route component around `dynamicComponent()`. Interaction that lives on the server — form posts to server actions via `action={addTodo}` — needs no client component at all.
|
|
429
429
|
|
|
430
430
|
### File-System Routes
|
|
431
431
|
|
|
@@ -517,7 +517,7 @@ Behavior modifiers are attributes, so they work identically in client, server-re
|
|
|
517
517
|
<a href="https://example.com">External — untouched</a>
|
|
518
518
|
```
|
|
519
519
|
|
|
520
|
-
Active and pending state is styled with CSS — one vocabulary for every kind of link:
|
|
520
|
+
Active and pending state is styled with CSS — one vocabulary for every kind of link. `aria-current` and `data-active` are automatic; `data-pending` is opt-in:
|
|
521
521
|
|
|
522
522
|
```css
|
|
523
523
|
nav a[aria-current="page"] {
|
|
@@ -528,9 +528,19 @@ nav a[data-active] {
|
|
|
528
528
|
} /* exact or prefix match on the path */
|
|
529
529
|
a[data-pending] {
|
|
530
530
|
opacity: 0.6;
|
|
531
|
-
} /* target of in-flight navigation */
|
|
531
|
+
} /* target of in-flight navigation — needs `links: pendingLinks` */
|
|
532
532
|
```
|
|
533
533
|
|
|
534
|
+
`data-pending` is opt-in, so an app that never shows pending link state doesn't ship the code that computes it. Pass the `pendingLinks` plugin to the router:
|
|
535
|
+
|
|
536
|
+
```tsx
|
|
537
|
+
import { createRouter, pendingLinks } from "@solidjs/router";
|
|
538
|
+
|
|
539
|
+
const Router = createRouter({ routes, links: pendingLinks });
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
It marks the links whose path covers the in-flight destination of a link click or `navigate()` call — by the same path rule as `data-active` — until that navigation lands. Back/forward traversals don't mark links pending. `data-pending` agrees with `useLinkState().pending` at every moment; the hook works with or without the plugin.
|
|
543
|
+
|
|
534
544
|
One rule decides both, for anchors and `useLinkState` alike:
|
|
535
545
|
|
|
536
546
|
- **current** (`aria-current="page"`) — same path and same query, ignoring parameter order and the hash. On `/?filter=active`, `<a href="/?filter=active">` is current and `<a href="/">` is not.
|
|
@@ -793,6 +803,7 @@ createRouter(config);
|
|
|
793
803
|
| `actionBase` | `string` | Root url for server actions, default `/_server` |
|
|
794
804
|
| `preloadLinks` | `boolean` | Preload route code/data on link hover and focus, default `true` |
|
|
795
805
|
| `explicitLinks` | `boolean` | Require the `link` attribute for router handling instead of intercepting all anchors, default `false` |
|
|
806
|
+
| `links` | `LinksPlugin` | Link claims plugin — `pendingLinks` adds `data-pending` to the in-flight navigation's target |
|
|
796
807
|
| `transformUrl` | `(url: string) => string` | Rewrite URLs before matching |
|
|
797
808
|
|
|
798
809
|
The returned instance is the provider component and carries the static surface:
|
|
@@ -850,14 +861,24 @@ See [Typed Search Params](#typed-search-params). Reads are proxied — access pr
|
|
|
850
861
|
|
|
851
862
|
### useIsRouting
|
|
852
863
|
|
|
853
|
-
A signal indicating whether the router is processing a navigation — useful for pending UI while the next route and its data settle:
|
|
864
|
+
A signal indicating whether the router is processing a navigation — useful for pending UI while the next route and its data settle. It's the way to read routing state: `RouterContext` doesn't carry `isRouting` or `pendingTarget`.
|
|
854
865
|
|
|
855
866
|
```tsx
|
|
856
867
|
const isRouting = useIsRouting();
|
|
857
868
|
return <div classList={{ "grey-out": isRouting() }}>...</div>;
|
|
858
869
|
```
|
|
859
870
|
|
|
860
|
-
|
|
871
|
+
The router doesn't expose where a navigation is heading; read it off the location with Solid's `isPending` and `latest`. It covers back/forward traversals too:
|
|
872
|
+
|
|
873
|
+
```tsx
|
|
874
|
+
import { isPending, latest } from "solid-js";
|
|
875
|
+
|
|
876
|
+
const location = useLocation();
|
|
877
|
+
const target = () =>
|
|
878
|
+
isPending(() => location.pathname) ? latest(() => location.pathname) : undefined;
|
|
879
|
+
```
|
|
880
|
+
|
|
881
|
+
In Solid's dev and observe builds the router also declares every navigation to the attribution engine (`solid-js/attribution`): holds and re-runs caused by a navigation are named after the route pattern (`navigation to /users/:id`), timed from the user event that started it, and redirect hops fold onto the navigation they belong to. A link click or a native form submit the router handles runs in the interaction Solid records for that event, so the navigation or action belongs to the same record as any `onClick` work for that click. `attribution.history("navigation")` and `feedback().navigations` list them; nothing of this exists in production builds.
|
|
861
882
|
|
|
862
883
|
### useMatch
|
|
863
884
|
|
|
@@ -905,7 +926,7 @@ Reactive `active`/`current`/`pending` state for [custom link components](#links)
|
|
|
905
926
|
|
|
906
927
|
- `current()` — same path and same query as the location, ignoring parameter order and the hash (what `aria-current="page"` reflects)
|
|
907
928
|
- `active()` — the location's path is the link's path or lives under it, query ignored (`data-active`); a root link (`/`, which resolves to the router's `base`) is exact-only
|
|
908
|
-
- `pending()` — the link's path is the target of an in-flight navigation (`data-pending`)
|
|
929
|
+
- `pending()` — the link's path is the target of an in-flight navigation, back/forward excluded (`data-pending`). Works without the [`pendingLinks`](#links) plugin, which only adds the attribute to plain anchors
|
|
909
930
|
|
|
910
931
|
Pass `{ end: true }` to make `active` (and `pending`) exact-path for any link.
|
|
911
932
|
|
|
@@ -1032,6 +1053,7 @@ Route props map 1:1 onto definition keys (`path`, `component`, `preload`, `match
|
|
|
1032
1053
|
- `<A href replace noScroll state>` → `<a href replace noscroll state>` (attributes, all lowercase)
|
|
1033
1054
|
- `activeClass` / `inactiveClass` → CSS attribute selectors on `[data-active]` / `[aria-current="page"]`
|
|
1034
1055
|
- `end` → style exact matches with `[aria-current="page"]` (which also compares the query) instead of `[data-active]`; the root path already only matches exactly
|
|
1056
|
+
- Pending link styling → `[data-pending]`, opt-in with `createRouter({ routes, links: pendingLinks })`
|
|
1035
1057
|
- Route-relative hrefs → typed `paths`; `useResolvedPath` / `useHref` remain for manual resolution
|
|
1036
1058
|
- Custom link components → `useLinkState`
|
|
1037
1059
|
|
|
@@ -1042,6 +1064,7 @@ Route props map 1:1 onto definition keys (`path`, `component`, `preload`, `match
|
|
|
1042
1064
|
- `redirect` / `reload` → import from `@solidjs/web`; they're protocol-level and work without the router
|
|
1043
1065
|
- `json(data, init)` → `respond(data, init)` from `@solidjs/web`
|
|
1044
1066
|
- `cache` (deprecated alias) → `query`
|
|
1067
|
+
- `RouterContext`'s `isRouting` / `pendingTarget` → `useIsRouting()`; for the in-flight destination, the [`isPending`/`latest` recipe](#useisrouting)
|
|
1045
1068
|
|
|
1046
1069
|
### Data APIs (Solid 2)
|
|
1047
1070
|
|
package/dist/claims.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouterContext } from "./types.js";
|
|
1
|
+
import type { LinksPlugin, RouterContext } from "./types.js";
|
|
2
2
|
export declare function setFormClaimHandler(handler: ((form: HTMLFormElement) => void) | undefined): void;
|
|
3
3
|
/**
|
|
4
4
|
* The compiler claims every `a[href]` and `form[action]` at creation, and the
|
|
@@ -11,7 +11,9 @@ export declare function setFormClaimHandler(handler: ((form: HTMLFormElement) =>
|
|
|
11
11
|
* included (parameter order aside)
|
|
12
12
|
* - `data-active` — pathname exact or prefix match (the router's root, its
|
|
13
13
|
* base path, exact only)
|
|
14
|
-
* - `data-pending` — the link is the target of an in-flight navigation
|
|
14
|
+
* - `data-pending` — the link is the target of an in-flight navigation;
|
|
15
|
+
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
16
|
+
* plugin, claims never read pending state and the sweep does not track it.
|
|
15
17
|
*
|
|
16
18
|
* The matching rule is `matchLink`, shared with `useLinkState`. The router
|
|
17
19
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
@@ -26,4 +28,4 @@ export declare function setFormClaimHandler(handler: ((form: HTMLFormElement) =>
|
|
|
26
28
|
* are the same one-shot untracked refresh, reading the element's current
|
|
27
29
|
* `href` from the DOM.
|
|
28
30
|
*/
|
|
29
|
-
export declare function setupLinkClaims(router: RouterContext, explicitLinks?: boolean): void;
|
|
31
|
+
export declare function setupLinkClaims(router: RouterContext, explicitLinks?: boolean, links?: LinksPlugin): void;
|
package/dist/claims.js
CHANGED
|
@@ -22,7 +22,9 @@ export function setFormClaimHandler(handler) {
|
|
|
22
22
|
* included (parameter order aside)
|
|
23
23
|
* - `data-active` — pathname exact or prefix match (the router's root, its
|
|
24
24
|
* base path, exact only)
|
|
25
|
-
* - `data-pending` — the link is the target of an in-flight navigation
|
|
25
|
+
* - `data-pending` — the link is the target of an in-flight navigation;
|
|
26
|
+
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
27
|
+
* plugin, claims never read pending state and the sweep does not track it.
|
|
26
28
|
*
|
|
27
29
|
* The matching rule is `matchLink`, shared with `useLinkState`. The router
|
|
28
30
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
@@ -37,8 +39,9 @@ export function setFormClaimHandler(handler) {
|
|
|
37
39
|
* are the same one-shot untracked refresh, reading the element's current
|
|
38
40
|
* `href` from the DOM.
|
|
39
41
|
*/
|
|
40
|
-
export function setupLinkClaims(router, explicitLinks) {
|
|
42
|
+
export function setupLinkClaims(router, explicitLinks, links) {
|
|
41
43
|
const basePath = router.base.path();
|
|
44
|
+
const plugin = links && links(router, basePath);
|
|
42
45
|
// per-element record; `owned` is whether the `aria-current` on the element
|
|
43
46
|
// is the router's, so it never writes over or removes an authored one
|
|
44
47
|
const claimed = new WeakMap();
|
|
@@ -76,22 +79,18 @@ export function setupLinkClaims(router, explicitLinks) {
|
|
|
76
79
|
// read reactive sources unconditionally so the owning effect stays
|
|
77
80
|
// subscribed even while the anchor is not router-managed
|
|
78
81
|
const location = router.location;
|
|
79
|
-
const routing =
|
|
82
|
+
const routing = plugin && plugin.track();
|
|
80
83
|
const url = managedUrl(a);
|
|
81
84
|
const target = url && url.pathname + url.search;
|
|
82
85
|
// no per-anchor `end` opt-out like useLinkState has
|
|
83
86
|
const { active, current } = matchLink(location, target, basePath);
|
|
84
|
-
|
|
85
|
-
// in-flight target comes from pendingTarget — readable here because the
|
|
86
|
-
// isRouting write flushes after the target is assigned
|
|
87
|
-
const pending = routing &&
|
|
88
|
-
!!router.pendingTarget &&
|
|
89
|
-
matchLink({ pathname: router.pendingTarget.value, search: "" }, target, basePath).active;
|
|
87
|
+
const pending = !!routing && plugin.pending(target);
|
|
90
88
|
return { active, pending, current };
|
|
91
89
|
}
|
|
92
90
|
function apply(a, rec, { active, pending, current }) {
|
|
93
91
|
active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
|
|
94
|
-
|
|
92
|
+
if (plugin)
|
|
93
|
+
pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
95
94
|
// Ownership is read against the element, not just the record. A
|
|
96
95
|
// server-component morph resets attributes to the server HTML, which
|
|
97
96
|
// never carries router link state, then re-claims: an owned value that
|
|
@@ -114,9 +113,10 @@ export function setupLinkClaims(router, explicitLinks) {
|
|
|
114
113
|
}
|
|
115
114
|
const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
|
|
116
115
|
// The one subscription for every anchor: compute tracks the sources
|
|
117
|
-
// linkState derives from (the
|
|
118
|
-
//
|
|
119
|
-
// assigned), the effect phase sweeps the
|
|
116
|
+
// linkState derives from (with the plugin, its pending read — the
|
|
117
|
+
// in-flight target is readable in the effect phase because the isRouting
|
|
118
|
+
// write flushes after the target is assigned), the effect phase sweeps the
|
|
119
|
+
// registry untracked.
|
|
120
120
|
//
|
|
121
121
|
// `transparent` keeps the effect invisible to the hydration id scheme.
|
|
122
122
|
// This setup is client-only, so an id-consuming node here has no server
|
|
@@ -124,7 +124,7 @@ export function setupLinkClaims(router, explicitLinks) {
|
|
|
124
124
|
// slot — lazy-route lookups miss and hydration leaves server nodes
|
|
125
125
|
// unclaimed. (The option is honored by the runtime but missing from the
|
|
126
126
|
// published EffectOptions type, hence the cast.)
|
|
127
|
-
createRenderEffect(() => (router.location.pathname, router.location.search,
|
|
127
|
+
createRenderEffect(() => (router.location.pathname, router.location.search, plugin && plugin.track()), () => registry.forEach(a => refresh(a, claimed.get(a))), { transparent: true });
|
|
128
128
|
onCleanup(registerElementClaim(node => {
|
|
129
129
|
const name = node.nodeName.toUpperCase();
|
|
130
130
|
if (name === "FORM")
|
package/dist/data/events.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { delegateEvents } from "@solidjs/web";
|
|
1
|
+
import { delegateEvents, dispatchAsInteraction } from "@solidjs/web";
|
|
2
2
|
import { onCleanup } from "solid-js";
|
|
3
3
|
import { isUnderBase } from "../utils.js";
|
|
4
4
|
let formHandler;
|
|
@@ -52,12 +52,14 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
52
52
|
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
53
53
|
const state = a.getAttribute("state");
|
|
54
54
|
evt.preventDefault();
|
|
55
|
-
|
|
55
|
+
// Only a click the router acts on joins the event's interaction: this
|
|
56
|
+
// listener hears every click on the document.
|
|
57
|
+
dispatchAsInteraction(evt, () => navigateFromRoute(to, {
|
|
56
58
|
resolve: false,
|
|
57
59
|
replace: a.hasAttribute("replace"),
|
|
58
60
|
scroll: !a.hasAttribute("noscroll"),
|
|
59
61
|
state: state ? JSON.parse(state) : undefined
|
|
60
|
-
});
|
|
62
|
+
}));
|
|
61
63
|
}
|
|
62
64
|
function handleAnchorPreload(evt) {
|
|
63
65
|
const res = handleAnchor(evt);
|
|
@@ -106,15 +108,17 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
106
108
|
const data = new FormData(form, evt.submitter);
|
|
107
109
|
import("./serverForms.js").then(m => m.submitServerForm(router, path, form, data));
|
|
108
110
|
}
|
|
111
|
+
const handleSubmit = (evt) => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
|
|
109
112
|
// ensure delegated event run first
|
|
110
113
|
delegateEvents(["click", "submit"]);
|
|
111
114
|
document.addEventListener("click", handleAnchorClick);
|
|
115
|
+
// preloads are not interactions: those listeners run outside any frame
|
|
112
116
|
if (preload) {
|
|
113
117
|
document.addEventListener("mousemove", handleAnchorMove, { passive: true });
|
|
114
118
|
document.addEventListener("focusin", handleAnchorPreload, { passive: true });
|
|
115
119
|
document.addEventListener("touchstart", handleAnchorPreload, { passive: true });
|
|
116
120
|
}
|
|
117
|
-
document.addEventListener("submit",
|
|
121
|
+
document.addEventListener("submit", handleSubmit);
|
|
118
122
|
onCleanup(() => {
|
|
119
123
|
document.removeEventListener("click", handleAnchorClick);
|
|
120
124
|
if (preload) {
|
|
@@ -122,7 +126,7 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
122
126
|
document.removeEventListener("focusin", handleAnchorPreload);
|
|
123
127
|
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
124
128
|
}
|
|
125
|
-
document.removeEventListener("submit",
|
|
129
|
+
document.removeEventListener("submit", handleSubmit);
|
|
126
130
|
});
|
|
127
131
|
};
|
|
128
132
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -21,9 +21,10 @@ export * from "./routers/index.js";
|
|
|
21
21
|
export * from "./lifecycle.js";
|
|
22
22
|
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
23
23
|
export type { LinkState } from "./routing.js";
|
|
24
|
+
export { pendingLinks } from "./pending.js";
|
|
24
25
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
25
26
|
export { int } from "./paths.js";
|
|
26
27
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
27
28
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
28
29
|
export * from "./data/index.js";
|
|
29
|
-
export type { Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, ServerRouteArgs, ServerRouteParams, ServerRouteFunction, ServerRouteView, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
|
30
|
+
export type { LinksPlugin, Location, LocationChange, LocationWrite, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, ServerRouteArgs, ServerRouteParams, ServerRouteFunction, ServerRouteView, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|