@solidjs/router 2.0.0-next.37 → 2.0.0-next.38
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 +74 -34
- package/dist/claims.d.ts +1 -1
- package/dist/claims.js +12 -3
- package/dist/data/events.d.ts +11 -4
- package/dist/data/events.js +37 -41
- package/dist/index.d.ts +4 -2
- package/dist/index.js +381 -155
- package/dist/index.jsx +3 -1
- package/dist/pending.js +2 -2
- package/dist/preload.d.ts +53 -0
- package/dist/preload.js +175 -0
- package/dist/preloadRoute.d.ts +23 -0
- package/dist/preloadRoute.js +92 -0
- package/dist/routers/factory.d.ts +13 -3
- package/dist/routers/factory.jsx +5 -2
- package/dist/routing.d.ts +1 -15
- package/dist/routing.js +11 -89
- package/dist/types.d.ts +19 -1
- package/dist/utils.d.ts +6 -5
- package/dist/utils.js +18 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
|
|
|
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
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
|
-
- **Preload Functions**: parallel data fetching following the render-as-you-fetch pattern, triggered eagerly
|
|
22
|
+
- **Preload Functions**: parallel data fetching following the render-as-you-fetch pattern, triggered eagerly by opt-in [link preloading](#preloading)
|
|
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
|
|
24
24
|
- **Typed Search Params**: opt-in per-route [Standard Schema](https://github.com/standard-schema/standard-schema) validation — `search.page` is a `number`, not `"2"`
|
|
25
25
|
|
|
@@ -40,6 +40,7 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
|
|
|
40
40
|
- [File-System Routes](#file-system-routes)
|
|
41
41
|
- [Typed Paths](#typed-paths)
|
|
42
42
|
- [Links](#links)
|
|
43
|
+
- [Preloading](#preloading)
|
|
43
44
|
- [Preload Functions](#preload-functions)
|
|
44
45
|
- [Data APIs](#data-apis)
|
|
45
46
|
- [Typed Search Params](#typed-search-params)
|
|
@@ -171,7 +172,7 @@ A route definition supports:
|
|
|
171
172
|
| `path` | `string \| string[]` | Path partial for this route segment |
|
|
172
173
|
| `component` | `Component` | Component rendered for the matched segment |
|
|
173
174
|
| `children` | `RouteDefinition \| RouteDefinition[] \| () => Promise<...>` | Nested route definitions, or a thunk for a [lazy subtree](#lazy-route-subtrees) |
|
|
174
|
-
| `preload` | `RoutePreloadFunc` | Called on
|
|
175
|
+
| `preload` | `RoutePreloadFunc` | Called on [link preloading](#preloading) and navigation |
|
|
175
176
|
| `matchFilters` | `MatchFilters` | Additional constraints for matching parameters |
|
|
176
177
|
| `search` | `StandardSchemaV1` | Search-param validator; its types flow into `paths` and hooks |
|
|
177
178
|
| `info` | `Record<string, any>` | Arbitrary metadata, readable via `useRouteMatches` |
|
|
@@ -360,11 +361,11 @@ const router = createRouter({
|
|
|
360
361
|
});
|
|
361
362
|
```
|
|
362
363
|
|
|
363
|
-
The import only fires when something needs the subtree —
|
|
364
|
+
The import only fires when something needs the subtree — preloading a link into it, navigating into it, or the server matching a URL beneath it. Until then the tree carries a placeholder that knows every URL under `/admin` belongs to the subtree without knowing its contents (static sibling routes still win without triggering the load). Everything folds in as if the routes were inline:
|
|
364
365
|
|
|
365
366
|
- **Types**: TypeScript never runs the thunk — inference flows through the import's promise type, so `paths.admin.users(2)` typechecks (match filters and search schemas included) before any of the subtree's code exists client-side. The module's `default` or `routes` export is used. Only tables genuinely built at runtime (typed as plain `RouteDefinition[]`) degrade to untyped.
|
|
366
367
|
- **Navigation**: the table load folds into the navigation transition — the old screen holds until the subtree (and its matched components) are ready, exactly like a `lazy()` route component.
|
|
367
|
-
- **Preloading**:
|
|
368
|
+
- **Preloading**: a link preload kicks the table load, and when it lands the preload continues into the inner routes' components and `preload` functions — one cascading warm-up from the earliest possible moment.
|
|
368
369
|
- **Server**: SSR resolves matched boundaries during the render (use the streaming entry point `renderToStream` — awaiting it resolves with the settled HTML — as with any async work), and the single-flight collector resolves them before its data pass.
|
|
369
370
|
|
|
370
371
|
Resolution is cached per thunk and append-only: the tree never changes shape after a subtree lands, it just gets more specific. Keep thunks deterministic — `() => import(...)` — rather than switching tables on runtime state.
|
|
@@ -399,10 +400,10 @@ The source is called with **derived** arguments, not a live location — the cal
|
|
|
399
400
|
|
|
400
401
|
A route view is route-shaped on purpose — the address stays stable and `defineRoute` can check its params against the pattern — which means it is only callable as a route. When the same server component is also used elsewhere, keep it a plain (non-exported, non-endpoint) function and have the route view call it with `params.id`. The value `serverRouteComponent` returns is a component only so it fits the `component` field; mounting it any other way (through `lazy()`, or by hand) throws, since outside the match there are only merged params to call with.
|
|
401
402
|
|
|
402
|
-
The router mounts the resolved component with the outlet as `children`, so a server component can be a layout, and it calls the same source under preload intent — link
|
|
403
|
+
The router mounts the resolved component with the outlet as `children`, so a server component can be a layout, and it calls the same source under preload intent — link preloading, `usePreloadRoute`, the [single-flight collector](#server-integration) — with the same derived args. What that call _means_ is the source's: the router does not choose the cache strategy or own the key.
|
|
403
404
|
|
|
404
405
|
- `query(fn, key)`: link intent warms the entry the render reads, `revalidate("story")` and action responses refetch it, and the collector reproduces it so a mutation's response carries the route's fresh markup. Argument changes deliver into the mounted boundary — it morphs in place rather than remounting.
|
|
405
|
-
- [`liveQuery(fn, key)`](#livequery-experimental): the frame stream stays open and the channel owns it —
|
|
406
|
+
- [`liveQuery(fn, key)`](#livequery-experimental): the frame stream stays open and the channel owns it — a link preload connects it (held through the preload window, so a preloaded link is an open stream), `revalidate(key)` reconnects, and the mutation sweep and the single-flight collector both leave it alone — nothing pulls it server-side, and the stream is its own freshness.
|
|
406
407
|
|
|
407
408
|
Anything else callable with the args works too; wrapping is what gives dedupe, preload, and revalidation.
|
|
408
409
|
|
|
@@ -502,14 +503,14 @@ There is no link component. Use `<a>`; the router intercepts same-origin clicks
|
|
|
502
503
|
|
|
503
504
|
Behavior modifiers are attributes, so they work identically in client, server-rendered, and third-party markup:
|
|
504
505
|
|
|
505
|
-
| attribute | description
|
|
506
|
-
| ---------- |
|
|
507
|
-
| `replace` | Replace the history entry instead of pushing
|
|
508
|
-
| `noscroll` | Turn off scrolling to the top after navigation
|
|
509
|
-
| `state` | JSON string [pushed](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) onto the history stack
|
|
510
|
-
| `preload` |
|
|
511
|
-
| `link` | Marks a router link when `explicitLinks` is enabled
|
|
512
|
-
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling
|
|
506
|
+
| attribute | description |
|
|
507
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------- |
|
|
508
|
+
| `replace` | Replace the history entry instead of pushing |
|
|
509
|
+
| `noscroll` | Turn off scrolling to the top after navigation |
|
|
510
|
+
| `state` | JSON string [pushed](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) onto the history stack |
|
|
511
|
+
| `preload` | `"false"` opts this link out of all [preloading](#preloading); `"viewport"` or `"eager"` opts it into that strategy |
|
|
512
|
+
| `link` | Marks a router link when `explicitLinks` is enabled |
|
|
513
|
+
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling |
|
|
513
514
|
|
|
514
515
|
```tsx
|
|
515
516
|
<a href={paths.login} replace>Log in</a>
|
|
@@ -563,9 +564,47 @@ function TabLink(props: { href: string; children: JSX.Element }) {
|
|
|
563
564
|
}
|
|
564
565
|
```
|
|
565
566
|
|
|
567
|
+
## Preloading
|
|
568
|
+
|
|
569
|
+
The router can warm a link's route before it is clicked: preloading **code** loads the matched routes' `lazy()` components (and any [lazy subtree](#lazy-route-subtrees) on the way), and preloading **data** also runs their [preload functions](#preload-functions) with `intent: "preload"`. Nothing is preloaded unless you opt in. Earlier `next` prereleases preloaded on hover, focus, and touch by default; that built-in is gone. A boolean `preloadLinks` is a type error and, in development, a warning — it preloads nothing.
|
|
570
|
+
|
|
571
|
+
```tsx
|
|
572
|
+
import { createRouter, intentPreload } from "@solidjs/router";
|
|
573
|
+
|
|
574
|
+
const Router = createRouter({ routes, preloadLinks: intentPreload() });
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
| strategy | preloads a link when | defaults |
|
|
578
|
+
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
|
|
579
|
+
| `intentPreload({ delay = 20, data = true })` | the pointer rests on it for `delay` ms, it is focused (`focusin`), or a touch starts on it (`touchstart`). Moving over a link that already preloaded does not preload it again until the pointer leaves | code and data |
|
|
580
|
+
| `tapPreload({ data = true })` | a pointer goes down on it (`pointerdown`: mouse, touch, or pen), ahead of the click | code and data |
|
|
581
|
+
| `viewportPreload({ all = false, data = false, delay = 100, rootMargin })` | it has stayed in the viewport for `delay` ms and the browser is then idle (`requestIdleCallback`, or `setTimeout` if that is missing). Once per `href`; leaving before the idle flush cancels it, and a new `href` preloads again. One shared `IntersectionObserver`. `rootMargin` is passed through (the observer's own `"0px"` when omitted) | code |
|
|
582
|
+
| `eagerPreload({ all = false, data = false })` | the page has loaded and the browser is idle, including links mounted later. Once per `href`; a link removed before that flush is dropped | code |
|
|
583
|
+
|
|
584
|
+
Pass an array to combine strategies — viewport for code, intent for data:
|
|
585
|
+
|
|
586
|
+
```tsx
|
|
587
|
+
const Router = createRouter({
|
|
588
|
+
routes,
|
|
589
|
+
preloadLinks: [viewportPreload({ all: true }), intentPreload()]
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Strategies are client-only, and an app ships only the ones it imports. `intentPreload` and `tapPreload` apply to every router link. `viewportPreload` and `eagerPreload` apply only to links that name them, `<a href="/pricing" preload="viewport">` or `preload="eager"`, unless created with `{ all: true }`. They also skip preloading when the browser reports Save-Data or a 2g connection (`effectiveType` of `"2g"` or `"slow-2g"`).
|
|
594
|
+
|
|
595
|
+
The `href` and `preload` attribute are read when the preload runs, so a dynamic `href` is the one the link has then.
|
|
596
|
+
|
|
597
|
+
Hover, focus, and touch preloads do not check modifier keys or `defaultPrevented`, and neither does `tapPreload`'s `pointerdown`. A click still does: the router ignores it when `preventDefault` was already called, when it is not the primary button, or when alt, ctrl, meta, or shift is held.
|
|
598
|
+
|
|
599
|
+
Preloading is purely an optimization: without it every navigation still loads the same code and data, just after the click instead of before it. A link with `preload="false"` is never preloaded by any strategy.
|
|
600
|
+
|
|
601
|
+
Whether a strategy also preloads data is its own `data` option, not a per-link choice. `intentPreload` and `tapPreload` default to data because the user is about to click; a data preload runs only for route levels the navigation would change, and `query` keeps the result for the few seconds until the click arrives. `viewportPreload` and `eagerPreload` warm links that are merely on screen or already on the page, so they default to code only — a data preload there is worth it only when the cache outlives the moment of clicking, as with TanStack Query or an HTTP-cached `GET` server function. Pass `{ data: true }` to opt in.
|
|
602
|
+
|
|
603
|
+
`usePreloadRoute` triggers the same work by hand.
|
|
604
|
+
|
|
566
605
|
## Preload Functions
|
|
567
606
|
|
|
568
|
-
Even with smart caches, waterfalls happen when data fetching waits on view logic or lazy-loaded code. Preload functions start fetching data in parallel with loading the route — called when a route renders, and eagerly when
|
|
607
|
+
Even with smart caches, waterfalls happen when data fetching waits on view logic or lazy-loaded code. Preload functions start fetching data in parallel with loading the route — called when a route renders, and eagerly when the router [preloads a link](#preloading) with data.
|
|
569
608
|
|
|
570
609
|
```tsx
|
|
571
610
|
import { lazy } from "solid-js";
|
|
@@ -581,11 +620,11 @@ const routes = defineRoutes([{ path: "/users/:id", component: User, preload: pre
|
|
|
581
620
|
|
|
582
621
|
The preload function receives:
|
|
583
622
|
|
|
584
|
-
| key | type | description
|
|
585
|
-
| -------- | -------------------------------------------------- |
|
|
586
|
-
| params | object | The route parameters (same value as `useParams()` inside the route component)
|
|
587
|
-
| location | `{ pathname, search, hash, query, state, key }` | Path information (corresponds to [`useLocation()`](#uselocation))
|
|
588
|
-
| intent | `"initial" \| "navigate" \| "native" \| "preload"` | Why this is being called: `initial` — first render; `navigate` — router navigation; `native` — browser back/forward; `preload` — link
|
|
623
|
+
| key | type | description |
|
|
624
|
+
| -------- | -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
625
|
+
| params | object | The route parameters (same value as `useParams()` inside the route component) |
|
|
626
|
+
| location | `{ pathname, search, hash, query, state, key }` | Path information (corresponds to [`useLocation()`](#uselocation)) |
|
|
627
|
+
| intent | `"initial" \| "navigate" \| "native" \| "preload"` | Why this is being called: `initial` — first render; `navigate` — router navigation; `native` — browser back/forward; `preload` — link preloading, not navigating |
|
|
589
628
|
|
|
590
629
|
The factory-level `preload` option is the app-wide counterpart: it runs once per mount/request with the merged params of every match, and its result reaches the root render-prop as `props.data`.
|
|
591
630
|
|
|
@@ -606,7 +645,7 @@ const getUser = query(async id => {
|
|
|
606
645
|
A query:
|
|
607
646
|
|
|
608
647
|
1. Dedupes on the server for the lifetime of the request.
|
|
609
|
-
2. Fills a preload cache in the browser lasting 5 seconds, so
|
|
648
|
+
2. Fills a preload cache in the browser lasting 5 seconds, so link preloads and route entry share one fetch.
|
|
610
649
|
3. Refetches reactively by key on action revalidation.
|
|
611
650
|
4. Serves as a back/forward cache for browser navigation up to 5 minutes; user-initiated navigation bypasses it.
|
|
612
651
|
|
|
@@ -793,18 +832,18 @@ Without a schema, `useSearchParams()` behaves as before: raw string values, merg
|
|
|
793
832
|
createRouter(config);
|
|
794
833
|
```
|
|
795
834
|
|
|
796
|
-
| option | type
|
|
797
|
-
| --------------- |
|
|
798
|
-
| `routes` | `RouteDefinition[]`
|
|
799
|
-
| `base` | `string`
|
|
800
|
-
| `preload` | `RoutePreloadFunc`
|
|
801
|
-
| `history` | `RouterHistory`
|
|
802
|
-
| `singleFlight` | `boolean`
|
|
803
|
-
| `actionBase` | `string`
|
|
804
|
-
| `preloadLinks` | `
|
|
805
|
-
| `explicitLinks` | `boolean`
|
|
806
|
-
| `links` | `LinksPlugin`
|
|
807
|
-
| `transformUrl` | `(url: string) => string`
|
|
835
|
+
| option | type | description |
|
|
836
|
+
| --------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
837
|
+
| `routes` | `RouteDefinition[]` | The route tree — inline arrays infer literally; wrap extracted trees in `defineRoutes` |
|
|
838
|
+
| `base` | `string` | Base url to use for matching routes |
|
|
839
|
+
| `preload` | `RoutePreloadFunc` | App-wide preload: once per mount/request, result reaches the root render-prop as `props.data` |
|
|
840
|
+
| `history` | `RouterHistory` | History adapter; defaults to browser history on the client and the request URL on the server |
|
|
841
|
+
| `singleFlight` | `boolean` | Single-flight mutations, default `true` |
|
|
842
|
+
| `actionBase` | `string` | Root url for server actions, default `/_server` |
|
|
843
|
+
| `preloadLinks` | `LinkPreload \| LinkPreload[]` | [Link preload strategies](#preloading), e.g. `intentPreload()`; none by default. A boolean is a type error |
|
|
844
|
+
| `explicitLinks` | `boolean` | Require the `link` attribute for router handling instead of intercepting all anchors, default `false` |
|
|
845
|
+
| `links` | `LinksPlugin` | Link claims plugin — `pendingLinks` adds `data-pending` to the in-flight navigation's target |
|
|
846
|
+
| `transformUrl` | `(url: string) => string` | Rewrite URLs before matching |
|
|
808
847
|
|
|
809
848
|
The returned instance is the provider component and carries the static surface:
|
|
810
849
|
|
|
@@ -913,7 +952,7 @@ declare module "@solidjs/router" {
|
|
|
913
952
|
|
|
914
953
|
### usePreloadRoute
|
|
915
954
|
|
|
916
|
-
Returns a function to preload a route manually — the same work link
|
|
955
|
+
Returns a function to preload a route manually — the same work [link preloading](#preloading) triggers automatically. Accepts strings, URLs, and typed path nodes:
|
|
917
956
|
|
|
918
957
|
```tsx
|
|
919
958
|
const preload = usePreloadRoute();
|
|
@@ -1056,6 +1095,7 @@ Route props map 1:1 onto definition keys (`path`, `component`, `preload`, `match
|
|
|
1056
1095
|
- Pending link styling → `[data-pending]`, opt-in with `createRouter({ routes, links: pendingLinks })`
|
|
1057
1096
|
- Route-relative hrefs → typed `paths`; `useResolvedPath` / `useHref` remain for manual resolution
|
|
1058
1097
|
- Custom link components → `useLinkState`
|
|
1098
|
+
- Hover/focus preloading is opt-in → `createRouter({ routes, preloadLinks: intentPreload() })`; `preload="false"` on a link skips it entirely. The previous `next` prerelease preloaded on hover by default
|
|
1059
1099
|
|
|
1060
1100
|
### Removed and renamed
|
|
1061
1101
|
|
package/dist/claims.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare function setFormClaimHandler(handler: ((form: HTMLFormElement) =>
|
|
|
15
15
|
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
16
16
|
* plugin, claims never read pending state and the sweep does not track it.
|
|
17
17
|
*
|
|
18
|
-
* The matching rule is `
|
|
18
|
+
* The matching rule is `linkMatcher`, shared with `useLinkState`. The router
|
|
19
19
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
20
20
|
* stepper's `"step"`, a static `"page"`) is left in place, current or not.
|
|
21
21
|
*
|
package/dist/claims.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { registerElementClaim } from "@solidjs/web";
|
|
2
2
|
import { createRenderEffect, getOwner, onCleanup, untrack } from "solid-js";
|
|
3
|
-
import { isUnderBase,
|
|
3
|
+
import { isUnderBase, linkMatcher } from "./utils.js";
|
|
4
4
|
/**
|
|
5
5
|
* Claimed forms are handed to this slot instead of the claims importing the
|
|
6
6
|
* action module: the action side installs it on first action creation (see
|
|
@@ -26,7 +26,7 @@ export function setFormClaimHandler(handler) {
|
|
|
26
26
|
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
27
27
|
* plugin, claims never read pending state and the sweep does not track it.
|
|
28
28
|
*
|
|
29
|
-
* The matching rule is `
|
|
29
|
+
* The matching rule is `linkMatcher`, shared with `useLinkState`. The router
|
|
30
30
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
31
31
|
* stepper's `"step"`, a static `"page"`) is left in place, current or not.
|
|
32
32
|
*
|
|
@@ -75,6 +75,10 @@ export function setupLinkClaims(router, explicitLinks, links) {
|
|
|
75
75
|
return;
|
|
76
76
|
return url;
|
|
77
77
|
}
|
|
78
|
+
// every anchor matches against the same location, so its parse is shared
|
|
79
|
+
// until the location changes
|
|
80
|
+
let matched;
|
|
81
|
+
let match;
|
|
78
82
|
function linkState(a) {
|
|
79
83
|
// read reactive sources unconditionally so the owning effect stays
|
|
80
84
|
// subscribed even while the anchor is not router-managed
|
|
@@ -82,8 +86,13 @@ export function setupLinkClaims(router, explicitLinks, links) {
|
|
|
82
86
|
const routing = plugin && plugin.track();
|
|
83
87
|
const url = managedUrl(a);
|
|
84
88
|
const target = url && url.pathname + url.search;
|
|
89
|
+
const key = location.pathname + location.search;
|
|
85
90
|
// no per-anchor `end` opt-out like useLinkState has
|
|
86
|
-
|
|
91
|
+
if (key !== matched) {
|
|
92
|
+
matched = key;
|
|
93
|
+
match = linkMatcher(location, basePath);
|
|
94
|
+
}
|
|
95
|
+
const { active, current } = match(target);
|
|
87
96
|
const pending = !!routing && plugin.pending(target);
|
|
88
97
|
return { active, pending, current };
|
|
89
98
|
}
|
package/dist/data/events.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouterContext } from "../types.js";
|
|
1
|
+
import type { LinkPreload, RouterContext } from "../types.js";
|
|
2
2
|
/**
|
|
3
3
|
* The submit delegation consults this slot instead of importing the action
|
|
4
4
|
* module: the action side installs its handler on first action creation
|
|
@@ -7,11 +7,18 @@ import type { RouterContext } from "../types.js";
|
|
|
7
7
|
*/
|
|
8
8
|
export type RouterFormHandler = (evt: SubmitEvent, router: RouterContext, actionBase: string) => void;
|
|
9
9
|
export declare function setRouterFormHandler(handler: RouterFormHandler | undefined): void;
|
|
10
|
+
/**
|
|
11
|
+
* Link preload strategies reach route preloading through this slot instead
|
|
12
|
+
* of the event wiring importing it: a strategy installs it when it runs (see
|
|
13
|
+
* preload.ts), so an app with no strategy and no `usePreloadRoute` never
|
|
14
|
+
* ships route preloading.
|
|
15
|
+
*/
|
|
16
|
+
declare let linkPreloader: ((router: RouterContext, url: URL, data: boolean) => void) | undefined;
|
|
17
|
+
export declare function setLinkPreloader(preloader: typeof linkPreloader): void;
|
|
10
18
|
type NativeEventConfig = {
|
|
11
|
-
preload?:
|
|
19
|
+
preload?: LinkPreload | readonly LinkPreload[];
|
|
12
20
|
explicitLinks?: boolean;
|
|
13
21
|
actionBase?: string;
|
|
14
|
-
transformUrl?: (url: string) => string;
|
|
15
22
|
};
|
|
16
|
-
export declare function setupNativeEvents({ preload, explicitLinks, actionBase
|
|
23
|
+
export declare function setupNativeEvents({ preload, explicitLinks, actionBase }?: NativeEventConfig): (router: RouterContext) => void;
|
|
17
24
|
export {};
|
package/dist/data/events.js
CHANGED
|
@@ -5,12 +5,20 @@ let formHandler;
|
|
|
5
5
|
export function setRouterFormHandler(handler) {
|
|
6
6
|
formHandler = handler;
|
|
7
7
|
}
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Link preload strategies reach route preloading through this slot instead
|
|
10
|
+
* of the event wiring importing it: a strategy installs it when it runs (see
|
|
11
|
+
* preload.ts), so an app with no strategy and no `usePreloadRoute` never
|
|
12
|
+
* ships route preloading.
|
|
13
|
+
*/
|
|
14
|
+
let linkPreloader;
|
|
15
|
+
export function setLinkPreloader(preloader) {
|
|
16
|
+
linkPreloader = preloader;
|
|
17
|
+
}
|
|
18
|
+
export function setupNativeEvents({ preload, explicitLinks = false, actionBase = "/_server" } = {}) {
|
|
9
19
|
return (router) => {
|
|
10
20
|
const basePath = router.base.path();
|
|
11
21
|
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
12
|
-
let preloadTimeout;
|
|
13
|
-
let lastElement;
|
|
14
22
|
function isSvg(el) {
|
|
15
23
|
return el.namespaceURI === "http://www.w3.org/2000/svg";
|
|
16
24
|
}
|
|
@@ -22,10 +30,19 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
22
30
|
evt.ctrlKey ||
|
|
23
31
|
evt.shiftKey)
|
|
24
32
|
return;
|
|
33
|
+
return findAnchor(evt);
|
|
34
|
+
}
|
|
35
|
+
// no button or modifier gate: focus and touch events carry neither
|
|
36
|
+
function findAnchor(evt) {
|
|
25
37
|
const a = evt
|
|
26
38
|
.composedPath()
|
|
27
39
|
.find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
|
|
28
|
-
|
|
40
|
+
const url = a && anchorUrl(a);
|
|
41
|
+
return url && [a, url];
|
|
42
|
+
}
|
|
43
|
+
/** The anchor's URL when the router manages it, else `undefined`. */
|
|
44
|
+
function anchorUrl(a) {
|
|
45
|
+
if (explicitLinks && !a.hasAttribute("link"))
|
|
29
46
|
return;
|
|
30
47
|
const svg = isSvg(a);
|
|
31
48
|
const href = svg ? a.href.baseVal : a.href;
|
|
@@ -42,7 +59,7 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
42
59
|
return;
|
|
43
60
|
if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath))
|
|
44
61
|
return;
|
|
45
|
-
return
|
|
62
|
+
return url;
|
|
46
63
|
}
|
|
47
64
|
function handleAnchorClick(evt) {
|
|
48
65
|
const res = handleAnchor(evt);
|
|
@@ -61,28 +78,6 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
61
78
|
state: state ? JSON.parse(state) : undefined
|
|
62
79
|
}));
|
|
63
80
|
}
|
|
64
|
-
function handleAnchorPreload(evt) {
|
|
65
|
-
const res = handleAnchor(evt);
|
|
66
|
-
if (!res)
|
|
67
|
-
return;
|
|
68
|
-
const [a, url] = res;
|
|
69
|
-
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
70
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
71
|
-
}
|
|
72
|
-
function handleAnchorMove(evt) {
|
|
73
|
-
clearTimeout(preloadTimeout);
|
|
74
|
-
const res = handleAnchor(evt);
|
|
75
|
-
if (!res)
|
|
76
|
-
return (lastElement = null);
|
|
77
|
-
const [a, url] = res;
|
|
78
|
-
if (lastElement === a)
|
|
79
|
-
return;
|
|
80
|
-
transformUrl && (url.pathname = transformUrl(url.pathname));
|
|
81
|
-
preloadTimeout = setTimeout(() => {
|
|
82
|
-
router.preloadRoute(url, a.getAttribute("preload") !== "false");
|
|
83
|
-
lastElement = a;
|
|
84
|
-
}, 20);
|
|
85
|
-
}
|
|
86
81
|
function handleFormSubmit(evt) {
|
|
87
82
|
if (formHandler)
|
|
88
83
|
return formHandler(evt, router, actionBase);
|
|
@@ -104,29 +99,30 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
|
|
|
104
99
|
const path = router.parsePath(url.pathname + url.search);
|
|
105
100
|
if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST")
|
|
106
101
|
return;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
102
|
+
// The import sits inside the check so bundlers drop the server-forms
|
|
103
|
+
// chunk. An early return above a still-present import() does not.
|
|
104
|
+
// Missing stays off: an undefined constant does not fold.
|
|
105
|
+
if (typeof __SOLID_SERVER_COMPONENTS__ !== "undefined" && __SOLID_SERVER_COMPONENTS__) {
|
|
106
|
+
evt.preventDefault();
|
|
107
|
+
const data = new FormData(form, evt.submitter);
|
|
108
|
+
import("./serverForms.js").then(m => m.submitServerForm(router, path, form, data));
|
|
109
|
+
}
|
|
110
110
|
}
|
|
111
111
|
const handleSubmit = (evt) => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
|
|
112
112
|
// ensure delegated event run first
|
|
113
113
|
delegateEvents(["click", "submit"]);
|
|
114
114
|
document.addEventListener("click", handleAnchorClick);
|
|
115
|
-
// preloads are not interactions: those listeners run outside any frame
|
|
116
|
-
if (preload) {
|
|
117
|
-
document.addEventListener("mousemove", handleAnchorMove, { passive: true });
|
|
118
|
-
document.addEventListener("focusin", handleAnchorPreload, { passive: true });
|
|
119
|
-
document.addEventListener("touchstart", handleAnchorPreload, { passive: true });
|
|
120
|
-
}
|
|
121
115
|
document.addEventListener("submit", handleSubmit);
|
|
122
116
|
onCleanup(() => {
|
|
123
117
|
document.removeEventListener("click", handleAnchorClick);
|
|
124
|
-
if (preload) {
|
|
125
|
-
document.removeEventListener("mousemove", handleAnchorMove);
|
|
126
|
-
document.removeEventListener("focusin", handleAnchorPreload);
|
|
127
|
-
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
128
|
-
}
|
|
129
118
|
document.removeEventListener("submit", handleSubmit);
|
|
130
119
|
});
|
|
120
|
+
// the pre-strategy boolean option is ignored (a dev warning names it)
|
|
121
|
+
if (preload && preload !== true)
|
|
122
|
+
[].concat(preload).forEach(strategy => strategy({
|
|
123
|
+
anchor: findAnchor,
|
|
124
|
+
url: anchorUrl,
|
|
125
|
+
preload: (url, data) => linkPreloader(router, url, data)
|
|
126
|
+
}));
|
|
131
127
|
};
|
|
132
128
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -19,12 +19,14 @@ export interface RouteInfo {
|
|
|
19
19
|
}
|
|
20
20
|
export * from "./routers/index.js";
|
|
21
21
|
export * from "./lifecycle.js";
|
|
22
|
-
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate,
|
|
22
|
+
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
23
23
|
export type { LinkState } from "./routing.js";
|
|
24
24
|
export { pendingLinks } from "./pending.js";
|
|
25
|
+
export { eagerPreload, intentPreload, tapPreload, viewportPreload } from "./preload.js";
|
|
26
|
+
export { usePreloadRoute } from "./preloadRoute.js";
|
|
25
27
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
26
28
|
export { int } from "./paths.js";
|
|
27
29
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|
|
28
30
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
29
31
|
export * from "./data/index.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";
|
|
32
|
+
export type { LinkPreload, 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";
|