@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/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack,
|
|
2
|
-
import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo,
|
|
1
|
+
import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, isPending, NotReadyError, DEV, latest, createContext, createSignal, useContext, createComponent, createOwner, createRoot, Show, onSettled, isHydrating, OBSERVE, getObserver, isHydratable, $TRACK, action as action$1 } from 'solid-js';
|
|
2
|
+
import { registerElementClaim, delegateEvents, dispatchAsInteraction, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamicComponent, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
|
|
3
3
|
import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
|
|
4
4
|
|
|
5
5
|
const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
|
|
@@ -249,7 +249,9 @@ function setFormClaimHandler(handler) {
|
|
|
249
249
|
* included (parameter order aside)
|
|
250
250
|
* - `data-active` — pathname exact or prefix match (the router's root, its
|
|
251
251
|
* base path, exact only)
|
|
252
|
-
* - `data-pending` — the link is the target of an in-flight navigation
|
|
252
|
+
* - `data-pending` — the link is the target of an in-flight navigation;
|
|
253
|
+
* opt-in through `createRouter({ links: pendingLinks })`. Without the
|
|
254
|
+
* plugin, claims never read pending state and the sweep does not track it.
|
|
253
255
|
*
|
|
254
256
|
* The matching rule is `matchLink`, shared with `useLinkState`. The router
|
|
255
257
|
* only touches an `aria-current` it wrote itself: one the author set (a
|
|
@@ -264,8 +266,9 @@ function setFormClaimHandler(handler) {
|
|
|
264
266
|
* are the same one-shot untracked refresh, reading the element's current
|
|
265
267
|
* `href` from the DOM.
|
|
266
268
|
*/
|
|
267
|
-
function setupLinkClaims(router, explicitLinks) {
|
|
269
|
+
function setupLinkClaims(router, explicitLinks, links) {
|
|
268
270
|
const basePath = router.base.path();
|
|
271
|
+
const plugin = links && links(router, basePath);
|
|
269
272
|
// per-element record; `owned` is whether the `aria-current` on the element
|
|
270
273
|
// is the router's, so it never writes over or removes an authored one
|
|
271
274
|
const claimed = new WeakMap();
|
|
@@ -299,7 +302,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
299
302
|
// read reactive sources unconditionally so the owning effect stays
|
|
300
303
|
// subscribed even while the anchor is not router-managed
|
|
301
304
|
const location = router.location;
|
|
302
|
-
const routing =
|
|
305
|
+
const routing = plugin && plugin.track();
|
|
303
306
|
const url = managedUrl(a);
|
|
304
307
|
const target = url && url.pathname + url.search;
|
|
305
308
|
// no per-anchor `end` opt-out like useLinkState has
|
|
@@ -307,13 +310,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
307
310
|
active,
|
|
308
311
|
current
|
|
309
312
|
} = matchLink(location, target, basePath);
|
|
310
|
-
|
|
311
|
-
// in-flight target comes from pendingTarget — readable here because the
|
|
312
|
-
// isRouting write flushes after the target is assigned
|
|
313
|
-
const pending = routing && !!router.pendingTarget && matchLink({
|
|
314
|
-
pathname: router.pendingTarget.value,
|
|
315
|
-
search: ""
|
|
316
|
-
}, target, basePath).active;
|
|
313
|
+
const pending = !!routing && plugin.pending(target);
|
|
317
314
|
return {
|
|
318
315
|
active,
|
|
319
316
|
pending,
|
|
@@ -326,7 +323,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
326
323
|
current
|
|
327
324
|
}) {
|
|
328
325
|
active ? a.setAttribute("data-active", "") : a.removeAttribute("data-active");
|
|
329
|
-
pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
326
|
+
if (plugin) pending ? a.setAttribute("data-pending", "") : a.removeAttribute("data-pending");
|
|
330
327
|
// Ownership is read against the element, not just the record. A
|
|
331
328
|
// server-component morph resets attributes to the server HTML, which
|
|
332
329
|
// never carries router link state, then re-claims: an owned value that
|
|
@@ -346,9 +343,10 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
346
343
|
const refresh = (a, rec) => untrack(() => apply(a, rec, linkState(a)));
|
|
347
344
|
|
|
348
345
|
// The one subscription for every anchor: compute tracks the sources
|
|
349
|
-
// linkState derives from (the
|
|
350
|
-
//
|
|
351
|
-
// assigned), the effect phase sweeps the
|
|
346
|
+
// linkState derives from (with the plugin, its pending read — the
|
|
347
|
+
// in-flight target is readable in the effect phase because the isRouting
|
|
348
|
+
// write flushes after the target is assigned), the effect phase sweeps the
|
|
349
|
+
// registry untracked.
|
|
352
350
|
//
|
|
353
351
|
// `transparent` keeps the effect invisible to the hydration id scheme.
|
|
354
352
|
// This setup is client-only, so an id-consuming node here has no server
|
|
@@ -356,7 +354,7 @@ function setupLinkClaims(router, explicitLinks) {
|
|
|
356
354
|
// slot — lazy-route lookups miss and hydration leaves server nodes
|
|
357
355
|
// unclaimed. (The option is honored by the runtime but missing from the
|
|
358
356
|
// published EffectOptions type, hence the cast.)
|
|
359
|
-
createRenderEffect(() => (router.location.pathname, router.location.search,
|
|
357
|
+
createRenderEffect(() => (router.location.pathname, router.location.search, plugin && plugin.track()), () => registry.forEach(a => refresh(a, claimed.get(a))), {
|
|
360
358
|
transparent: true
|
|
361
359
|
});
|
|
362
360
|
onCleanup(registerElementClaim(node => {
|
|
@@ -432,12 +430,14 @@ function setupNativeEvents({
|
|
|
432
430
|
const to = router.parsePath(url.pathname + url.search + url.hash);
|
|
433
431
|
const state = a.getAttribute("state");
|
|
434
432
|
evt.preventDefault();
|
|
435
|
-
|
|
433
|
+
// Only a click the router acts on joins the event's interaction: this
|
|
434
|
+
// listener hears every click on the document.
|
|
435
|
+
dispatchAsInteraction(evt, () => navigateFromRoute(to, {
|
|
436
436
|
resolve: false,
|
|
437
437
|
replace: a.hasAttribute("replace"),
|
|
438
438
|
scroll: !a.hasAttribute("noscroll"),
|
|
439
439
|
state: state ? JSON.parse(state) : undefined
|
|
440
|
-
});
|
|
440
|
+
}));
|
|
441
441
|
}
|
|
442
442
|
function handleAnchorPreload(evt) {
|
|
443
443
|
const res = handleAnchor(evt);
|
|
@@ -477,10 +477,12 @@ function setupNativeEvents({
|
|
|
477
477
|
const data = new FormData(form, evt.submitter);
|
|
478
478
|
Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
|
|
479
479
|
}
|
|
480
|
+
const handleSubmit = evt => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
|
|
480
481
|
|
|
481
482
|
// ensure delegated event run first
|
|
482
483
|
delegateEvents(["click", "submit"]);
|
|
483
484
|
document.addEventListener("click", handleAnchorClick);
|
|
485
|
+
// preloads are not interactions: those listeners run outside any frame
|
|
484
486
|
if (preload) {
|
|
485
487
|
document.addEventListener("mousemove", handleAnchorMove, {
|
|
486
488
|
passive: true
|
|
@@ -492,7 +494,7 @@ function setupNativeEvents({
|
|
|
492
494
|
passive: true
|
|
493
495
|
});
|
|
494
496
|
}
|
|
495
|
-
document.addEventListener("submit",
|
|
497
|
+
document.addEventListener("submit", handleSubmit);
|
|
496
498
|
onCleanup(() => {
|
|
497
499
|
document.removeEventListener("click", handleAnchorClick);
|
|
498
500
|
if (preload) {
|
|
@@ -500,7 +502,7 @@ function setupNativeEvents({
|
|
|
500
502
|
document.removeEventListener("focusin", handleAnchorPreload);
|
|
501
503
|
document.removeEventListener("touchstart", handleAnchorPreload);
|
|
502
504
|
}
|
|
503
|
-
document.removeEventListener("submit",
|
|
505
|
+
document.removeEventListener("submit", handleSubmit);
|
|
504
506
|
});
|
|
505
507
|
};
|
|
506
508
|
}
|
|
@@ -610,7 +612,7 @@ function createPathsProxy(renderPath = p => p, base = "") {
|
|
|
610
612
|
|
|
611
613
|
// The light half of server component routes: the brand the router core reads
|
|
612
614
|
// off a route `component`, and the derivation of a route's call arguments.
|
|
613
|
-
// Deliberately free of `query`/`
|
|
615
|
+
// Deliberately free of `query`/`dynamicComponent` — routing.ts imports this module,
|
|
614
616
|
// and routing.ts is in every app's eager graph. The heavy half (the helper
|
|
615
617
|
// that builds the branded component) lives in serverRouteComponent.ts and only
|
|
616
618
|
// enters a bundle when an app calls `serverRouteComponent()`.
|
|
@@ -674,6 +676,86 @@ function serverRouteArgsEqual(a, b) {
|
|
|
674
676
|
return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
|
|
675
677
|
}
|
|
676
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Pending navigation state, for the opt-in readers only: `useIsRouting`,
|
|
681
|
+
* `useLinkState().pending` and the `pendingLinks` claims plugin. `isPending`
|
|
682
|
+
* and `latest` are UI affordances — the router's own coordination reads its
|
|
683
|
+
* location writes and their `onSettled` instead — so an app that renders
|
|
684
|
+
* none of these never bundles Solid's verdict machinery.
|
|
685
|
+
*/
|
|
686
|
+
|
|
687
|
+
const routing = new WeakMap();
|
|
688
|
+
|
|
689
|
+
/** Whether a navigation is in flight: one memo per router, under its owner. */
|
|
690
|
+
function routingState(router) {
|
|
691
|
+
let read = routing.get(router);
|
|
692
|
+
if (!read) {
|
|
693
|
+
const {
|
|
694
|
+
location,
|
|
695
|
+
matches,
|
|
696
|
+
_source: source
|
|
697
|
+
} = router;
|
|
698
|
+
// `transparent`: created on first use rather than at router setup, so it
|
|
699
|
+
// must not take a hydration id — the server may never create it, or
|
|
700
|
+
// create it at a different point.
|
|
701
|
+
const pending = runWithOwner(router._owner, () => createMemo(() => isPending(() => {
|
|
702
|
+
try {
|
|
703
|
+
matches();
|
|
704
|
+
} catch (e) {
|
|
705
|
+
if (e instanceof NotReadyError) throw e;
|
|
706
|
+
}
|
|
707
|
+
location.search;
|
|
708
|
+
location.hash;
|
|
709
|
+
}), {
|
|
710
|
+
transparent: true,
|
|
711
|
+
...(DEV && {
|
|
712
|
+
name: "routingPending"
|
|
713
|
+
})
|
|
714
|
+
}));
|
|
715
|
+
read = () => pending() || isPending(source);
|
|
716
|
+
routing.set(router, read);
|
|
717
|
+
}
|
|
718
|
+
return read;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* The target of the in-flight programmatic navigation, if any. A
|
|
723
|
+
* back/forward traversal (`_navigation` -1) is not a link's target.
|
|
724
|
+
*/
|
|
725
|
+
function pendingTarget(router) {
|
|
726
|
+
if (!routingState(router)()) return;
|
|
727
|
+
const target = latest(router._source);
|
|
728
|
+
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/** Whether `to` is the destination of the in-flight navigation. */
|
|
732
|
+
function linkPending(router, to, base, end) {
|
|
733
|
+
const target = pendingTarget(router);
|
|
734
|
+
return !!target && matchLink({
|
|
735
|
+
pathname: target.value,
|
|
736
|
+
search: ""
|
|
737
|
+
}, to, base, end).active;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Opt-in `data-pending` for plain anchors. Marks claimed links whose path
|
|
742
|
+
* covers the in-flight destination of a link click or `navigate()` (not
|
|
743
|
+
* back/forward), until it lands. Agrees with `useLinkState().pending`, which
|
|
744
|
+
* works without it. `aria-current` and `data-active` need no plugin.
|
|
745
|
+
*
|
|
746
|
+
* @example
|
|
747
|
+
* ```ts
|
|
748
|
+
* import { createRouter, pendingLinks } from "@solidjs/router";
|
|
749
|
+
*
|
|
750
|
+
* const Router = createRouter({ routes, links: pendingLinks });
|
|
751
|
+
* // CSS: a[data-pending] { opacity: .6 }
|
|
752
|
+
* ```
|
|
753
|
+
*/
|
|
754
|
+
const pendingLinks = (router, base) => ({
|
|
755
|
+
track: routingState(router),
|
|
756
|
+
pending: target => linkPending(router, target, base)
|
|
757
|
+
});
|
|
758
|
+
|
|
677
759
|
const MAX_REDIRECTS = 100;
|
|
678
760
|
|
|
679
761
|
/**
|
|
@@ -759,6 +841,8 @@ const useLocation = () => useRouter().location;
|
|
|
759
841
|
/**
|
|
760
842
|
* Retrieves a signal that indicates whether the router is currently processing a navigation.
|
|
761
843
|
* Useful for showing pending navigation state while the next route and its data settle.
|
|
844
|
+
* This is the way to read routing state: `RouterContext` doesn't carry `isRouting`.
|
|
845
|
+
* For the in-flight destination, read the location with Solid's `isPending`/`latest`.
|
|
762
846
|
*
|
|
763
847
|
* @example
|
|
764
848
|
* ```js
|
|
@@ -771,7 +855,7 @@ const useLocation = () => useRouter().location;
|
|
|
771
855
|
* );
|
|
772
856
|
* ```
|
|
773
857
|
*/
|
|
774
|
-
const useIsRouting = () => useRouter()
|
|
858
|
+
const useIsRouting = () => routingState(useRouter());
|
|
775
859
|
|
|
776
860
|
/**
|
|
777
861
|
* `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
|
|
@@ -959,10 +1043,7 @@ const useLinkState = (href, options = {}) => {
|
|
|
959
1043
|
// observe the committed location during a transition
|
|
960
1044
|
pending: createMemo(() => {
|
|
961
1045
|
state(); // location dependency: mid-flight target swaps recompute
|
|
962
|
-
return router
|
|
963
|
-
pathname: router.pendingTarget.value,
|
|
964
|
-
search: ""
|
|
965
|
-
}, to(), base, options.end).active;
|
|
1046
|
+
return linkPending(router, to(), base, options.end);
|
|
966
1047
|
})
|
|
967
1048
|
};
|
|
968
1049
|
};
|
|
@@ -1320,7 +1401,12 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1320
1401
|
_navigation: isServer ? undefined : 1
|
|
1321
1402
|
});
|
|
1322
1403
|
}
|
|
1323
|
-
|
|
1404
|
+
|
|
1405
|
+
// The flushed world — what `latest(source)` answers — as the location's own
|
|
1406
|
+
// memo sees it: it recomputes under the transition that carries a write,
|
|
1407
|
+
// never for a write of this same tick (A28).
|
|
1408
|
+
let flushed = initialSource;
|
|
1409
|
+
const location = createLocation(() => (flushed = source()).value, () => source().state, utils.queryWrapper);
|
|
1324
1410
|
// The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
|
|
1325
1411
|
// must be appended before streaming flushes the response headers, and an
|
|
1326
1412
|
// unread outcome must not haunt a later request's render. Only detection
|
|
@@ -1397,7 +1483,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1397
1483
|
// NotReadyError's source must be a reactive async node, not the raw
|
|
1398
1484
|
// Promise. Keep one reader per boundary for this router owner so rejection
|
|
1399
1485
|
// is delivered through the graph's error channel and resolution wakes the
|
|
1400
|
-
// parked matches computation.
|
|
1486
|
+
// parked matches computation. `transparent`: the reader exists on the
|
|
1487
|
+
// client only (the server parks `matches` on the promise directly), so it
|
|
1488
|
+
// must not take a hydration id the server never allocated.
|
|
1401
1489
|
const routerOwner = getOwner();
|
|
1402
1490
|
const lazyReaders = new WeakMap();
|
|
1403
1491
|
const readLazySubtree = record => {
|
|
@@ -1406,8 +1494,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1406
1494
|
read = runWithOwner(routerOwner, () => createMemo(() => {
|
|
1407
1495
|
const result = resolveLazySubtree(record);
|
|
1408
1496
|
return result instanceof Promise ? result.then(() => undefined) : undefined;
|
|
1409
|
-
},
|
|
1410
|
-
|
|
1497
|
+
}, {
|
|
1498
|
+
transparent: true,
|
|
1499
|
+
...(DEV && {
|
|
1500
|
+
name: "lazyRoutes"
|
|
1501
|
+
})
|
|
1411
1502
|
}));
|
|
1412
1503
|
lazyReaders.set(record, read);
|
|
1413
1504
|
}
|
|
@@ -1442,28 +1533,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1442
1533
|
}, DEV && {
|
|
1443
1534
|
name: "matches"
|
|
1444
1535
|
});
|
|
1445
|
-
const
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
} catch (e) {
|
|
1449
|
-
if (e instanceof NotReadyError) throw e;
|
|
1450
|
-
}
|
|
1451
|
-
location.search;
|
|
1452
|
-
location.hash;
|
|
1453
|
-
}), DEV && {
|
|
1454
|
-
name: "routingPending"
|
|
1455
|
-
});
|
|
1456
|
-
const isRouting = () => routingPending() || isPending(source);
|
|
1536
|
+
const headedLocation = () => isServer ? untrack(source) : flushed;
|
|
1537
|
+
// A flushed navigation write whose settle has not run is still in flight.
|
|
1538
|
+
const unsettled = change => change._navigation !== undefined && !!integration.settled && !integration.settled(change);
|
|
1457
1539
|
const transitionIntent = () => {
|
|
1458
|
-
|
|
1459
|
-
|
|
1540
|
+
const head = headedLocation();
|
|
1541
|
+
if (!unsettled(head)) return;
|
|
1542
|
+
const navigation = head._navigation;
|
|
1460
1543
|
return navigation === -1 ? "native" : navigation && navigation > 0 ? "navigate" : undefined;
|
|
1461
1544
|
};
|
|
1462
|
-
const pendingNavigation = () => {
|
|
1463
|
-
if (!isRouting()) return;
|
|
1464
|
-
const target = latest(source);
|
|
1465
|
-
return target._navigation && target._navigation > 0 ? target : undefined;
|
|
1466
|
-
};
|
|
1467
1545
|
const buildParams = () => mergeParams(matches());
|
|
1468
1546
|
const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
|
|
1469
1547
|
const params = wrapParams(buildParams);
|
|
@@ -1481,11 +1559,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1481
1559
|
location,
|
|
1482
1560
|
params,
|
|
1483
1561
|
wrapParams,
|
|
1484
|
-
isRouting,
|
|
1485
1562
|
intent: transitionIntent,
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
},
|
|
1563
|
+
_source: source,
|
|
1564
|
+
_owner: routerOwner,
|
|
1489
1565
|
renderPath,
|
|
1490
1566
|
parsePath,
|
|
1491
1567
|
navigatorFactory,
|
|
@@ -1546,10 +1622,10 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1546
1622
|
// the destination the leave guard is told all read it. A write of this
|
|
1547
1623
|
// same tick is not in it (A28) — see `compose` below.
|
|
1548
1624
|
//
|
|
1549
|
-
// A redirect hop: the previous navigation
|
|
1550
|
-
//
|
|
1551
|
-
//
|
|
1552
|
-
const headed =
|
|
1625
|
+
// A redirect hop: the previous navigation's write has not settled — it
|
|
1626
|
+
// is still held, or a guard is redirecting in its landing flush — so its
|
|
1627
|
+
// destination was never shown.
|
|
1628
|
+
const headed = headedLocation();
|
|
1553
1629
|
|
|
1554
1630
|
// A composed target (`setSearchParams`) is a function of where the
|
|
1555
1631
|
// router is heading, an unflushed write of this tick included. Only
|
|
@@ -1579,7 +1655,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
|
|
|
1579
1655
|
resolvedTo = resolveTarget(to);
|
|
1580
1656
|
if (resolvedTo === undefined) throw unroutable(to);
|
|
1581
1657
|
}
|
|
1582
|
-
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && (
|
|
1658
|
+
const navigationDepth = !isServer && headed._navigation !== undefined && headed._navigation > 0 && unsettled(headed) ? headed._navigation : 0;
|
|
1583
1659
|
if (navigationDepth >= MAX_REDIRECTS) {
|
|
1584
1660
|
throw new Error("Too many redirects");
|
|
1585
1661
|
}
|
|
@@ -1833,9 +1909,12 @@ function Routes(props) {
|
|
|
1833
1909
|
// they stay subscribed to `matches` and crash on a later navigation (#451)
|
|
1834
1910
|
onCleanup(() => disposers.forEach(dispose => dispose()));
|
|
1835
1911
|
// Route roots must outlive re-runs of the `routeStates` memo below, so they
|
|
1836
|
-
// are created under
|
|
1837
|
-
// computation (which disposes its children every time it re-runs).
|
|
1838
|
-
|
|
1912
|
+
// are created under an owner of this component rather than the memo's
|
|
1913
|
+
// computation (which disposes its children every time it re-runs). A
|
|
1914
|
+
// dedicated one, created before the memo: the roots draw hydration ids from
|
|
1915
|
+
// its counter, so the ids don't depend on whether the memo first parked on
|
|
1916
|
+
// a lazy subtree (the server's render can park where the client's doesn't).
|
|
1917
|
+
const owner = createOwner();
|
|
1839
1918
|
const routeStates = createMemo(prev => {
|
|
1840
1919
|
// While a lazy subtree resolves, `matches()` is not ready and this
|
|
1841
1920
|
// computation parks with it — no route contexts are created against
|
|
@@ -2127,6 +2206,7 @@ function memoryHistory(initial = "/") {
|
|
|
2127
2206
|
}
|
|
2128
2207
|
|
|
2129
2208
|
const STORAGE_KEY = "solid-router:scroll";
|
|
2209
|
+
const AUTO_KEY = "solid-router:scroll-auto";
|
|
2130
2210
|
|
|
2131
2211
|
/**
|
|
2132
2212
|
* Explicit scroll restoration for back/forward navigation. The browser's
|
|
@@ -2141,7 +2221,7 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
2141
2221
|
* so restoration survives reloads, which `scrollRestoration = "manual"`
|
|
2142
2222
|
* otherwise disables.
|
|
2143
2223
|
*
|
|
2144
|
-
* Restoration is a single scroll once
|
|
2224
|
+
* Restoration is a single scroll once the traversal settles — the same strategy
|
|
2145
2225
|
* SvelteKit, TanStack Router and React Router use. Settling after the
|
|
2146
2226
|
* transition commits is what makes the offset reachable; chasing a still-
|
|
2147
2227
|
* growing document afterwards (a ResizeObserver re-asserting the offset as
|
|
@@ -2151,17 +2231,49 @@ const STORAGE_KEY = "solid-router:scroll";
|
|
|
2151
2231
|
* layout changes can feed it back into itself. Content that commits after the
|
|
2152
2232
|
* transition settles — an image without reserved space, a boundary below the
|
|
2153
2233
|
* fold — keeps whatever offset the document can hold.
|
|
2234
|
+
*
|
|
2235
|
+
* A server-rendered document load is the exception: the browser restores it
|
|
2236
|
+
* natively once the document — streamed boundaries included — has loaded,
|
|
2237
|
+
* which a restore at the first client flush cannot match. So an entry is
|
|
2238
|
+
* handed back to the browser (`auto`) whenever its document may unload
|
|
2239
|
+
* (pagehide), and a hydrating router arriving on a handed-back entry leaves
|
|
2240
|
+
* the restore to the browser: it does not scroll, and it takes the entry back
|
|
2241
|
+
* (`manual`) only after load or at the first client navigation, whichever
|
|
2242
|
+
* comes first. A programmatic scroll during the load cancels the native
|
|
2243
|
+
* restore in Firefox and WebKit, and WebKit restores just after the load
|
|
2244
|
+
* event, so a `manual` write before then — inside a load listener included —
|
|
2245
|
+
* suppresses it; the write waits a task. Handed-back depths are tracked here
|
|
2246
|
+
* rather than read from `history.scrollRestoration`, which Firefox reports as
|
|
2247
|
+
* the mode before pagehide after a reload. An entry this router pushed and
|
|
2248
|
+
* never handed back stays `manual` and gets no native restore, so the router
|
|
2249
|
+
* restores it as on a client-rendered load.
|
|
2154
2250
|
*/
|
|
2155
|
-
function createScrollRestoration() {
|
|
2156
|
-
window.history
|
|
2251
|
+
function createScrollRestoration(hydrating) {
|
|
2252
|
+
const h = window.history;
|
|
2157
2253
|
// the current entry needs its depth stamp for captures to have a key, even
|
|
2158
2254
|
// if something replaced history.state after the adapter stamped it
|
|
2159
2255
|
saveCurrentDepth();
|
|
2256
|
+
const depth = () => window.history.state && window.history.state._depth;
|
|
2160
2257
|
let positions = {};
|
|
2258
|
+
let handedBack = {};
|
|
2161
2259
|
try {
|
|
2162
2260
|
positions = JSON.parse(sessionStorage.getItem(STORAGE_KEY)) || {};
|
|
2261
|
+
handedBack = JSON.parse(sessionStorage.getItem(AUTO_KEY)) || {};
|
|
2163
2262
|
} catch {}
|
|
2164
|
-
const
|
|
2263
|
+
const manual = () => {
|
|
2264
|
+
h.scrollRestoration = "manual";
|
|
2265
|
+
const d = depth();
|
|
2266
|
+
if (d != null) delete handedBack[d];
|
|
2267
|
+
};
|
|
2268
|
+
const d = depth();
|
|
2269
|
+
let deferred = !!hydrating && (h.scrollRestoration === "auto" || d != null && !!handedBack[d]);
|
|
2270
|
+
if (!deferred) manual();
|
|
2271
|
+
const claim = () => {
|
|
2272
|
+
if (!deferred) return;
|
|
2273
|
+
deferred = false;
|
|
2274
|
+
manual();
|
|
2275
|
+
};
|
|
2276
|
+
let timer;
|
|
2165
2277
|
let programmatic = false;
|
|
2166
2278
|
let pending;
|
|
2167
2279
|
const unbind = [bindEvent(window, "scroll", () => {
|
|
@@ -2170,10 +2282,16 @@ function createScrollRestoration() {
|
|
|
2170
2282
|
// the user took over — a pending restore would yank them
|
|
2171
2283
|
if (!programmatic) pending = undefined;
|
|
2172
2284
|
}), bindEvent(window, "pagehide", () => {
|
|
2285
|
+
h.scrollRestoration = "auto";
|
|
2286
|
+
const d = depth();
|
|
2287
|
+
if (d != null) handedBack[d] = 1;
|
|
2173
2288
|
try {
|
|
2174
2289
|
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(positions));
|
|
2290
|
+
sessionStorage.setItem(AUTO_KEY, JSON.stringify(handedBack));
|
|
2175
2291
|
} catch {}
|
|
2176
|
-
})
|
|
2292
|
+
}), bindEvent(window, "pageshow", e => {
|
|
2293
|
+
if (e.persisted) manual();
|
|
2294
|
+
}), bindEvent(window, "load", () => deferred && (timer = setTimeout(claim))), () => clearTimeout(timer)];
|
|
2177
2295
|
const restore = () => {
|
|
2178
2296
|
if (pending == null) return;
|
|
2179
2297
|
const y = positions[pending];
|
|
@@ -2186,42 +2304,38 @@ function createScrollRestoration() {
|
|
|
2186
2304
|
programmatic = false;
|
|
2187
2305
|
};
|
|
2188
2306
|
return {
|
|
2189
|
-
/**
|
|
2307
|
+
/**
|
|
2308
|
+
* When the adapter notifies a traversal: mark the target and restore once
|
|
2309
|
+
* the transition carrying it settles. The adapter notifies right before
|
|
2310
|
+
* the location write, in the same tick, so the settle is that write's —
|
|
2311
|
+
* a same-URL traversal included, which no location key would see.
|
|
2312
|
+
*/
|
|
2190
2313
|
onPop() {
|
|
2314
|
+
claim();
|
|
2191
2315
|
pending = depth();
|
|
2316
|
+
runWithOwner(null, () => onSettled(restore));
|
|
2192
2317
|
},
|
|
2318
|
+
/** The initial page settled: restore a reload/back_forward arrival. */
|
|
2319
|
+
settled: () => restore(),
|
|
2320
|
+
/** Before a client navigation writes history: the entry it creates copies the current mode. */
|
|
2321
|
+
beforeWrite: claim,
|
|
2193
2322
|
/** After a push: forward entries died, and this depth may be reused. */
|
|
2194
2323
|
onPush() {
|
|
2195
2324
|
const d = depth();
|
|
2196
|
-
if (d != null)
|
|
2325
|
+
if (d != null) {
|
|
2326
|
+
for (const k in positions) +k >= d && delete positions[k];
|
|
2327
|
+
for (const k in handedBack) +k >= d && delete handedBack[k];
|
|
2328
|
+
}
|
|
2197
2329
|
},
|
|
2198
|
-
create(
|
|
2199
|
-
// Restore once the traversal has settled: key on the location (a fully
|
|
2200
|
-
// synchronous pop commits without isRouting ever flipping) and on
|
|
2201
|
-
// isRouting, which reports in-flight transitions — native pops
|
|
2202
|
-
// included — and holds the restore until they commit. restore() no-ops
|
|
2203
|
-
// unless a traversal marked a target, so push navigations are inert.
|
|
2204
|
-
// `transparent` keeps the effect invisible to the hydration id scheme —
|
|
2205
|
-
// same reasoning as the link-claims effect (claims.ts): this setup is
|
|
2206
|
-
// client-only, so an id-consuming node here has no server counterpart
|
|
2207
|
-
// and every hydration id allocated after it shifts by one child slot.
|
|
2208
|
-
// The visible failure is any <Loading> content that settled before the
|
|
2209
|
-
// shell flush (a cache hit, a preloaded query): its serialized value and
|
|
2210
|
-
// inlined markup are keyed under the server's ids, the shifted client
|
|
2211
|
-
// misses both, recomputes, and re-renders the route fresh — duplicating
|
|
2212
|
-
// the server DOM and leaving it inert.
|
|
2213
|
-
createEffect(() => ({
|
|
2214
|
-
url: router.location.pathname + router.location.search + router.location.hash,
|
|
2215
|
-
routing: router.isRouting()
|
|
2216
|
-
}), current => {
|
|
2217
|
-
if (!current.routing) restore();
|
|
2218
|
-
}, {
|
|
2219
|
-
transparent: true
|
|
2220
|
-
});
|
|
2330
|
+
create() {
|
|
2221
2331
|
onCleanup(() => unbind.forEach(u => u()));
|
|
2222
2332
|
// reload/back_forward document loads land on an existing entry (a fresh
|
|
2223
|
-
// navigation starts a new one and belongs at the top); the
|
|
2224
|
-
// initial
|
|
2333
|
+
// navigation starts a new one and belongs at the top); the router calls
|
|
2334
|
+
// `settled` once the initial page has; a deferred arrival is the browser's
|
|
2335
|
+
if (deferred) {
|
|
2336
|
+
document.readyState === "complete" && claim();
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2225
2339
|
const [nav] = performance.getEntriesByType?.("navigation");
|
|
2226
2340
|
if (nav && nav.type !== "navigate") pending = depth();
|
|
2227
2341
|
}
|
|
@@ -2237,6 +2351,7 @@ function withScrollRestoration(history, restoration) {
|
|
|
2237
2351
|
return {
|
|
2238
2352
|
...history,
|
|
2239
2353
|
set(next) {
|
|
2354
|
+
restoration.beforeWrite();
|
|
2240
2355
|
history.set(next);
|
|
2241
2356
|
next.replace || restoration.onPush();
|
|
2242
2357
|
},
|
|
@@ -2384,8 +2499,8 @@ function describeInitial(match, location) {
|
|
|
2384
2499
|
/** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
|
|
2385
2500
|
function createIntegration(history, match) {
|
|
2386
2501
|
let committing = false;
|
|
2387
|
-
//
|
|
2388
|
-
|
|
2502
|
+
// Writes whose transition has landed (see `RouterIntegration.settled`).
|
|
2503
|
+
const settled = new WeakSet();
|
|
2389
2504
|
const wrap = value => typeof value === "string" ? {
|
|
2390
2505
|
value
|
|
2391
2506
|
} : value;
|
|
@@ -2404,14 +2519,16 @@ function createIntegration(history, match) {
|
|
|
2404
2519
|
// the no-op rule compares against it, so `navigate()` behind another
|
|
2405
2520
|
// write in one handler sees that write rather than the flushed world.
|
|
2406
2521
|
write(headed => (written = resolveLocationWrite(headed, next)) || headed);
|
|
2407
|
-
if (written && written._navigation
|
|
2522
|
+
if (written && written._navigation) {
|
|
2408
2523
|
const next = written;
|
|
2409
|
-
inflight = next;
|
|
2410
2524
|
// Register out of band so a destination error boundary replacing the
|
|
2411
|
-
// Router subtree cannot suppress the winning history commit.
|
|
2525
|
+
// Router subtree cannot suppress the winning history commit. The
|
|
2526
|
+
// settle is tied to the transition carrying the write: it fires when
|
|
2527
|
+
// that lands, a rejection included, and a superseded write's settle
|
|
2528
|
+
// fires with the transition that absorbed it.
|
|
2412
2529
|
runWithOwner(null, () => onSettled(() => {
|
|
2413
|
-
|
|
2414
|
-
if (read() !== next) return;
|
|
2530
|
+
settled.add(next);
|
|
2531
|
+
if (next._navigation < 0 || read() !== next) return;
|
|
2415
2532
|
committing = true;
|
|
2416
2533
|
try {
|
|
2417
2534
|
history.set(next);
|
|
@@ -2445,7 +2562,7 @@ function createIntegration(history, match) {
|
|
|
2445
2562
|
}));
|
|
2446
2563
|
return {
|
|
2447
2564
|
signal,
|
|
2448
|
-
|
|
2565
|
+
settled: write => settled.has(write),
|
|
2449
2566
|
utils: history.utils
|
|
2450
2567
|
};
|
|
2451
2568
|
}
|
|
@@ -2510,7 +2627,7 @@ function createRouter(config) {
|
|
|
2510
2627
|
let restoration;
|
|
2511
2628
|
let history = config.history;
|
|
2512
2629
|
if (!isServer && (config.scrollRestoration ?? !history)) {
|
|
2513
|
-
restoration = createScrollRestoration();
|
|
2630
|
+
restoration = createScrollRestoration(isHydrating());
|
|
2514
2631
|
history = withScrollRestoration(history || browserHistory(), restoration);
|
|
2515
2632
|
}
|
|
2516
2633
|
const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
|
|
@@ -2530,10 +2647,14 @@ function createRouter(config) {
|
|
|
2530
2647
|
actionBase: config.actionBase,
|
|
2531
2648
|
transformUrl: config.transformUrl
|
|
2532
2649
|
})(routerState);
|
|
2533
|
-
setupLinkClaims(routerState, config.explicitLinks);
|
|
2650
|
+
setupLinkClaims(routerState, config.explicitLinks, config.links);
|
|
2534
2651
|
if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
|
|
2535
|
-
restoration && restoration.create(
|
|
2652
|
+
restoration && restoration.create();
|
|
2536
2653
|
}
|
|
2654
|
+
// Registered on both sides, outside the client-only branch: an owned
|
|
2655
|
+
// onSettled takes a hydration id on the server too, so the ids line up.
|
|
2656
|
+
// A no-op when the arrival is left to the browser's native restore.
|
|
2657
|
+
onSettled(() => restoration && restoration.settled());
|
|
2537
2658
|
return createComponent$1(RouterContextObj, {
|
|
2538
2659
|
value: routerState,
|
|
2539
2660
|
get children() {
|
|
@@ -2680,7 +2801,7 @@ const useBeforeLeave = listener => {
|
|
|
2680
2801
|
// // before: a client component exists only to make the call
|
|
2681
2802
|
// const getStory = query(storyView, "story");
|
|
2682
2803
|
// component: props => {
|
|
2683
|
-
// const View =
|
|
2804
|
+
// const View = dynamicComponent(() => getStory(props.params.id));
|
|
2684
2805
|
// return <View>{props.children}</View>;
|
|
2685
2806
|
// }
|
|
2686
2807
|
// // after
|
|
@@ -2742,8 +2863,9 @@ source) {
|
|
|
2742
2863
|
};
|
|
2743
2864
|
function render(args, routeProps) {
|
|
2744
2865
|
// The source may answer a component, a promise of one, or (a live query)
|
|
2745
|
-
// successive components; `
|
|
2746
|
-
|
|
2866
|
+
// successive components; `dynamicComponent`'s memo lands each the same
|
|
2867
|
+
// way. Never a tag, so the element runtime `dynamic` retains stays out.
|
|
2868
|
+
const View = dynamicComponent(() => call(args()));
|
|
2747
2869
|
return createComponent(View, {
|
|
2748
2870
|
get children() {
|
|
2749
2871
|
return routeProps.children;
|
|
@@ -4098,4 +4220,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
|
|
|
4098
4220
|
submitServerForm: submitServerForm
|
|
4099
4221
|
});
|
|
4100
4222
|
|
|
4101
|
-
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
|
4223
|
+
export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, pendingLinks, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
|
package/dist/index.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./routers/index.js";
|
|
2
2
|
export * from "./lifecycle.js";
|
|
3
3
|
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
4
|
+
export { pendingLinks } from "./pending.js";
|
|
4
5
|
export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
5
6
|
export { int } from "./paths.js";
|
|
6
7
|
export { serverRouteComponent } from "./serverRouteComponent.js";
|