@remix-run/router 1.9.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/router.cjs.js +103 -12
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +15 -1
- package/dist/router.js +96 -6
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +103 -12
- package/dist/router.umd.js.map +1 -1
- package/dist/router.umd.min.js +2 -2
- package/dist/router.umd.min.js.map +1 -1
- package/dist/utils.d.ts +12 -4
- package/package.json +1 -1
- package/router.ts +168 -23
- package/utils.ts +13 -8
package/dist/utils.d.ts
CHANGED
|
@@ -54,8 +54,8 @@ export type DataResult = SuccessResult | DeferredResult | RedirectResult | Error
|
|
|
54
54
|
type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
55
55
|
type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
|
|
56
56
|
/**
|
|
57
|
-
* Users can specify either lowercase or uppercase form methods on
|
|
58
|
-
* useSubmit(),
|
|
57
|
+
* Users can specify either lowercase or uppercase form methods on `<Form>`,
|
|
58
|
+
* useSubmit(), `<fetcher.Form>`, etc.
|
|
59
59
|
*/
|
|
60
60
|
export type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;
|
|
61
61
|
/**
|
|
@@ -470,11 +470,20 @@ export declare const redirect: RedirectFunction;
|
|
|
470
470
|
* Defaults to "302 Found".
|
|
471
471
|
*/
|
|
472
472
|
export declare const redirectDocument: RedirectFunction;
|
|
473
|
+
export type ErrorResponse = {
|
|
474
|
+
status: number;
|
|
475
|
+
statusText: string;
|
|
476
|
+
data: any;
|
|
477
|
+
};
|
|
473
478
|
/**
|
|
474
479
|
* @private
|
|
475
480
|
* Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
|
|
481
|
+
*
|
|
482
|
+
* We don't export the class for public use since it's an implementation
|
|
483
|
+
* detail, but we export the interface above so folks can build their own
|
|
484
|
+
* abstractions around instances via isRouteErrorResponse()
|
|
476
485
|
*/
|
|
477
|
-
export declare class ErrorResponseImpl {
|
|
486
|
+
export declare class ErrorResponseImpl implements ErrorResponse {
|
|
478
487
|
status: number;
|
|
479
488
|
statusText: string;
|
|
480
489
|
data: any;
|
|
@@ -482,7 +491,6 @@ export declare class ErrorResponseImpl {
|
|
|
482
491
|
private internal;
|
|
483
492
|
constructor(status: number, statusText: string | undefined, data: any, internal?: boolean);
|
|
484
493
|
}
|
|
485
|
-
export type ErrorResponse = InstanceType<typeof ErrorResponseImpl>;
|
|
486
494
|
/**
|
|
487
495
|
* Check if the given error is an ErrorResponse generated from a 4xx/5xx
|
|
488
496
|
* Response thrown from an action/loader
|
package/package.json
CHANGED
package/router.ts
CHANGED
|
@@ -80,6 +80,14 @@ export interface Router {
|
|
|
80
80
|
*/
|
|
81
81
|
get routes(): AgnosticDataRouteObject[];
|
|
82
82
|
|
|
83
|
+
/**
|
|
84
|
+
* @internal
|
|
85
|
+
* PRIVATE - DO NOT USE
|
|
86
|
+
*
|
|
87
|
+
* Return the window associated with the router
|
|
88
|
+
*/
|
|
89
|
+
get window(): RouterInit["window"];
|
|
90
|
+
|
|
83
91
|
/**
|
|
84
92
|
* @internal
|
|
85
93
|
* PRIVATE - DO NOT USE
|
|
@@ -388,11 +396,21 @@ export interface StaticHandler {
|
|
|
388
396
|
): Promise<any>;
|
|
389
397
|
}
|
|
390
398
|
|
|
399
|
+
type ViewTransitionOpts = {
|
|
400
|
+
currentLocation: Location;
|
|
401
|
+
nextLocation: Location;
|
|
402
|
+
};
|
|
403
|
+
|
|
391
404
|
/**
|
|
392
405
|
* Subscriber function signature for changes to router state
|
|
393
406
|
*/
|
|
394
407
|
export interface RouterSubscriber {
|
|
395
|
-
(
|
|
408
|
+
(
|
|
409
|
+
state: RouterState,
|
|
410
|
+
opts: {
|
|
411
|
+
unstable_viewTransitionOpts?: ViewTransitionOpts;
|
|
412
|
+
}
|
|
413
|
+
): void;
|
|
396
414
|
}
|
|
397
415
|
|
|
398
416
|
/**
|
|
@@ -423,6 +441,7 @@ type BaseNavigateOptions = BaseNavigateOrFetchOptions & {
|
|
|
423
441
|
replace?: boolean;
|
|
424
442
|
state?: any;
|
|
425
443
|
fromRouteId?: string;
|
|
444
|
+
unstable_viewTransition?: boolean;
|
|
426
445
|
};
|
|
427
446
|
|
|
428
447
|
// Only allowed for submission navigations
|
|
@@ -690,6 +709,8 @@ const defaultMapRouteProperties: MapRoutePropertiesFunction = (route) => ({
|
|
|
690
709
|
hasErrorBoundary: Boolean(route.hasErrorBoundary),
|
|
691
710
|
});
|
|
692
711
|
|
|
712
|
+
const TRANSITIONS_STORAGE_KEY = "remix-router-transitions";
|
|
713
|
+
|
|
693
714
|
//#endregion
|
|
694
715
|
|
|
695
716
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -814,6 +835,18 @@ export function createRouter(init: RouterInit): Router {
|
|
|
814
835
|
// AbortController for the active navigation
|
|
815
836
|
let pendingNavigationController: AbortController | null;
|
|
816
837
|
|
|
838
|
+
// Should the current navigation enable document.startViewTransition?
|
|
839
|
+
let pendingViewTransitionEnabled = false;
|
|
840
|
+
|
|
841
|
+
// Store applied view transitions so we can apply them on POP
|
|
842
|
+
let appliedViewTransitions: Map<string, Set<string>> = new Map<
|
|
843
|
+
string,
|
|
844
|
+
Set<string>
|
|
845
|
+
>();
|
|
846
|
+
|
|
847
|
+
// Cleanup function for persisting applied transitions to sessionStorage
|
|
848
|
+
let removePageHideEventListener: (() => void) | null = null;
|
|
849
|
+
|
|
817
850
|
// We use this to avoid touching history in completeNavigation if a
|
|
818
851
|
// revalidation is entirely uninterrupted
|
|
819
852
|
let isUninterruptedRevalidation = false;
|
|
@@ -929,6 +962,17 @@ export function createRouter(init: RouterInit): Router {
|
|
|
929
962
|
}
|
|
930
963
|
);
|
|
931
964
|
|
|
965
|
+
if (isBrowser) {
|
|
966
|
+
// FIXME: This feels gross. How can we cleanup the lines between
|
|
967
|
+
// scrollRestoration/appliedTransitions persistance?
|
|
968
|
+
restoreAppliedTransitions(routerWindow, appliedViewTransitions);
|
|
969
|
+
let _saveAppliedTransitions = () =>
|
|
970
|
+
persistAppliedTransitions(routerWindow, appliedViewTransitions);
|
|
971
|
+
routerWindow.addEventListener("pagehide", _saveAppliedTransitions);
|
|
972
|
+
removePageHideEventListener = () =>
|
|
973
|
+
routerWindow.removeEventListener("pagehide", _saveAppliedTransitions);
|
|
974
|
+
}
|
|
975
|
+
|
|
932
976
|
// Kick off initial data load if needed. Use Pop to avoid modifying history
|
|
933
977
|
// Note we don't do any handling of lazy here. For SPA's it'll get handled
|
|
934
978
|
// in the normal navigation flow. For SSR it's expected that lazy modules are
|
|
@@ -946,6 +990,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
946
990
|
if (unlistenHistory) {
|
|
947
991
|
unlistenHistory();
|
|
948
992
|
}
|
|
993
|
+
if (removePageHideEventListener) {
|
|
994
|
+
removePageHideEventListener();
|
|
995
|
+
}
|
|
949
996
|
subscribers.clear();
|
|
950
997
|
pendingNavigationController && pendingNavigationController.abort();
|
|
951
998
|
state.fetchers.forEach((_, key) => deleteFetcher(key));
|
|
@@ -959,12 +1006,17 @@ export function createRouter(init: RouterInit): Router {
|
|
|
959
1006
|
}
|
|
960
1007
|
|
|
961
1008
|
// Update our state and notify the calling context of the change
|
|
962
|
-
function updateState(
|
|
1009
|
+
function updateState(
|
|
1010
|
+
newState: Partial<RouterState>,
|
|
1011
|
+
viewTransitionOpts?: ViewTransitionOpts
|
|
1012
|
+
): void {
|
|
963
1013
|
state = {
|
|
964
1014
|
...state,
|
|
965
1015
|
...newState,
|
|
966
1016
|
};
|
|
967
|
-
subscribers.forEach((subscriber) =>
|
|
1017
|
+
subscribers.forEach((subscriber) =>
|
|
1018
|
+
subscriber(state, { unstable_viewTransitionOpts: viewTransitionOpts })
|
|
1019
|
+
);
|
|
968
1020
|
}
|
|
969
1021
|
|
|
970
1022
|
// Complete a navigation returning the state.navigation back to the IDLE_NAVIGATION
|
|
@@ -1045,26 +1097,64 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1045
1097
|
init.history.replace(location, location.state);
|
|
1046
1098
|
}
|
|
1047
1099
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
location
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1100
|
+
let viewTransitionOpts: ViewTransitionOpts | undefined;
|
|
1101
|
+
|
|
1102
|
+
// On POP, enable transitions if they were enabled on the original navigation
|
|
1103
|
+
if (pendingAction === HistoryAction.Pop) {
|
|
1104
|
+
// Forward takes precedence so they behave like the original navigation
|
|
1105
|
+
let priorPaths = appliedViewTransitions.get(state.location.pathname);
|
|
1106
|
+
if (priorPaths && priorPaths.has(location.pathname)) {
|
|
1107
|
+
viewTransitionOpts = {
|
|
1108
|
+
currentLocation: state.location,
|
|
1109
|
+
nextLocation: location,
|
|
1110
|
+
};
|
|
1111
|
+
} else if (appliedViewTransitions.has(location.pathname)) {
|
|
1112
|
+
// If we don't have a previous forward nav, assume we're popping back to
|
|
1113
|
+
// the new location and enable if that location previously enabled
|
|
1114
|
+
viewTransitionOpts = {
|
|
1115
|
+
currentLocation: location,
|
|
1116
|
+
nextLocation: state.location,
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
} else if (pendingViewTransitionEnabled) {
|
|
1120
|
+
// Store the applied transition on PUSH/REPLACE
|
|
1121
|
+
let toPaths = appliedViewTransitions.get(state.location.pathname);
|
|
1122
|
+
if (toPaths) {
|
|
1123
|
+
toPaths.add(location.pathname);
|
|
1124
|
+
} else {
|
|
1125
|
+
toPaths = new Set<string>([location.pathname]);
|
|
1126
|
+
appliedViewTransitions.set(state.location.pathname, toPaths);
|
|
1127
|
+
}
|
|
1128
|
+
viewTransitionOpts = {
|
|
1129
|
+
currentLocation: state.location,
|
|
1130
|
+
nextLocation: location,
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
updateState(
|
|
1135
|
+
{
|
|
1136
|
+
...newState, // matches, errors, fetchers go through as-is
|
|
1137
|
+
actionData,
|
|
1138
|
+
loaderData,
|
|
1139
|
+
historyAction: pendingAction,
|
|
1058
1140
|
location,
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1141
|
+
initialized: true,
|
|
1142
|
+
navigation: IDLE_NAVIGATION,
|
|
1143
|
+
revalidation: "idle",
|
|
1144
|
+
restoreScrollPosition: getSavedScrollPosition(
|
|
1145
|
+
location,
|
|
1146
|
+
newState.matches || state.matches
|
|
1147
|
+
),
|
|
1148
|
+
preventScrollReset,
|
|
1149
|
+
blockers,
|
|
1150
|
+
},
|
|
1151
|
+
viewTransitionOpts
|
|
1152
|
+
);
|
|
1064
1153
|
|
|
1065
1154
|
// Reset stateful navigation vars
|
|
1066
1155
|
pendingAction = HistoryAction.Pop;
|
|
1067
1156
|
pendingPreventScrollReset = false;
|
|
1157
|
+
pendingViewTransitionEnabled = false;
|
|
1068
1158
|
isUninterruptedRevalidation = false;
|
|
1069
1159
|
isRevalidationRequired = false;
|
|
1070
1160
|
cancelledDeferredRoutes = [];
|
|
@@ -1173,6 +1263,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1173
1263
|
pendingError: error,
|
|
1174
1264
|
preventScrollReset,
|
|
1175
1265
|
replace: opts && opts.replace,
|
|
1266
|
+
enableViewTransition: opts && opts.unstable_viewTransition,
|
|
1176
1267
|
});
|
|
1177
1268
|
}
|
|
1178
1269
|
|
|
@@ -1223,6 +1314,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1223
1314
|
startUninterruptedRevalidation?: boolean;
|
|
1224
1315
|
preventScrollReset?: boolean;
|
|
1225
1316
|
replace?: boolean;
|
|
1317
|
+
enableViewTransition?: boolean;
|
|
1226
1318
|
}
|
|
1227
1319
|
): Promise<void> {
|
|
1228
1320
|
// Abort any in-progress navigations and start a new one. Unset any ongoing
|
|
@@ -1239,6 +1331,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1239
1331
|
saveScrollPosition(state.location, state.matches);
|
|
1240
1332
|
pendingPreventScrollReset = (opts && opts.preventScrollReset) === true;
|
|
1241
1333
|
|
|
1334
|
+
pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true;
|
|
1335
|
+
|
|
1242
1336
|
let routesToUse = inFlightDataRoutes || dataRoutes;
|
|
1243
1337
|
let loadingNavigation = opts && opts.overrideNavigation;
|
|
1244
1338
|
let matches = matchRoutes(routesToUse, location, basename);
|
|
@@ -2505,6 +2599,9 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2505
2599
|
get routes() {
|
|
2506
2600
|
return dataRoutes;
|
|
2507
2601
|
},
|
|
2602
|
+
get window() {
|
|
2603
|
+
return routerWindow;
|
|
2604
|
+
},
|
|
2508
2605
|
initialize,
|
|
2509
2606
|
subscribe,
|
|
2510
2607
|
enableScrollRestoration,
|
|
@@ -3074,7 +3171,7 @@ export function getStaticContextFromError(
|
|
|
3074
3171
|
}
|
|
3075
3172
|
|
|
3076
3173
|
function isSubmissionNavigation(
|
|
3077
|
-
opts:
|
|
3174
|
+
opts: BaseNavigateOrFetchOptions
|
|
3078
3175
|
): opts is SubmissionNavigateOptions {
|
|
3079
3176
|
return (
|
|
3080
3177
|
opts != null &&
|
|
@@ -3158,7 +3255,7 @@ function normalizeNavigateOptions(
|
|
|
3158
3255
|
normalizeFormMethod: boolean,
|
|
3159
3256
|
isFetcher: boolean,
|
|
3160
3257
|
path: string,
|
|
3161
|
-
opts?:
|
|
3258
|
+
opts?: BaseNavigateOrFetchOptions
|
|
3162
3259
|
): {
|
|
3163
3260
|
path: string;
|
|
3164
3261
|
submission?: Submission;
|
|
@@ -4074,9 +4171,12 @@ function getShortCircuitMatches(routes: AgnosticDataRouteObject[]): {
|
|
|
4074
4171
|
route: AgnosticDataRouteObject;
|
|
4075
4172
|
} {
|
|
4076
4173
|
// Prefer a root layout route if present, otherwise shim in a route object
|
|
4077
|
-
let route =
|
|
4078
|
-
|
|
4079
|
-
|
|
4174
|
+
let route =
|
|
4175
|
+
routes.length === 1
|
|
4176
|
+
? routes[0]
|
|
4177
|
+
: routes.find((r) => r.index || !r.path || r.path === "/") || {
|
|
4178
|
+
id: `__shim-error-route__`,
|
|
4179
|
+
};
|
|
4080
4180
|
|
|
4081
4181
|
return {
|
|
4082
4182
|
matches: [
|
|
@@ -4492,4 +4592,49 @@ function getDoneFetcher(data: Fetcher["data"]): FetcherStates["Idle"] {
|
|
|
4492
4592
|
};
|
|
4493
4593
|
return fetcher;
|
|
4494
4594
|
}
|
|
4595
|
+
|
|
4596
|
+
function restoreAppliedTransitions(
|
|
4597
|
+
_window: Window,
|
|
4598
|
+
transitions: Map<string, Set<string>>
|
|
4599
|
+
) {
|
|
4600
|
+
try {
|
|
4601
|
+
let sessionPositions = _window.sessionStorage.getItem(
|
|
4602
|
+
TRANSITIONS_STORAGE_KEY
|
|
4603
|
+
);
|
|
4604
|
+
if (sessionPositions) {
|
|
4605
|
+
let json = JSON.parse(sessionPositions);
|
|
4606
|
+
for (let [k, v] of Object.entries(json || {})) {
|
|
4607
|
+
if (v && Array.isArray(v)) {
|
|
4608
|
+
transitions.set(k, new Set(v || []));
|
|
4609
|
+
}
|
|
4610
|
+
}
|
|
4611
|
+
}
|
|
4612
|
+
} catch (e) {
|
|
4613
|
+
// no-op, use default empty object
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
|
|
4617
|
+
function persistAppliedTransitions(
|
|
4618
|
+
_window: Window,
|
|
4619
|
+
transitions: Map<string, Set<string>>
|
|
4620
|
+
) {
|
|
4621
|
+
if (transitions.size > 0) {
|
|
4622
|
+
let json: Record<string, string[]> = {};
|
|
4623
|
+
for (let [k, v] of transitions) {
|
|
4624
|
+
json[k] = [...v];
|
|
4625
|
+
}
|
|
4626
|
+
try {
|
|
4627
|
+
_window.sessionStorage.setItem(
|
|
4628
|
+
TRANSITIONS_STORAGE_KEY,
|
|
4629
|
+
JSON.stringify(json)
|
|
4630
|
+
);
|
|
4631
|
+
} catch (error) {
|
|
4632
|
+
warning(
|
|
4633
|
+
false,
|
|
4634
|
+
`Failed to save applied view transitions in sessionStorage (${error}).`
|
|
4635
|
+
);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4495
4640
|
//#endregion
|
package/utils.ts
CHANGED
|
@@ -68,8 +68,8 @@ type LowerCaseFormMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
|
68
68
|
type UpperCaseFormMethod = Uppercase<LowerCaseFormMethod>;
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Users can specify either lowercase or uppercase form methods on
|
|
72
|
-
* useSubmit(),
|
|
71
|
+
* Users can specify either lowercase or uppercase form methods on `<Form>`,
|
|
72
|
+
* useSubmit(), `<fetcher.Form>`, etc.
|
|
73
73
|
*/
|
|
74
74
|
export type HTMLFormMethod = LowerCaseFormMethod | UpperCaseFormMethod;
|
|
75
75
|
|
|
@@ -1533,11 +1533,21 @@ export const redirectDocument: RedirectFunction = (url, init) => {
|
|
|
1533
1533
|
return response;
|
|
1534
1534
|
};
|
|
1535
1535
|
|
|
1536
|
+
export type ErrorResponse = {
|
|
1537
|
+
status: number;
|
|
1538
|
+
statusText: string;
|
|
1539
|
+
data: any;
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1536
1542
|
/**
|
|
1537
1543
|
* @private
|
|
1538
1544
|
* Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
|
|
1545
|
+
*
|
|
1546
|
+
* We don't export the class for public use since it's an implementation
|
|
1547
|
+
* detail, but we export the interface above so folks can build their own
|
|
1548
|
+
* abstractions around instances via isRouteErrorResponse()
|
|
1539
1549
|
*/
|
|
1540
|
-
export class ErrorResponseImpl {
|
|
1550
|
+
export class ErrorResponseImpl implements ErrorResponse {
|
|
1541
1551
|
status: number;
|
|
1542
1552
|
statusText: string;
|
|
1543
1553
|
data: any;
|
|
@@ -1562,11 +1572,6 @@ export class ErrorResponseImpl {
|
|
|
1562
1572
|
}
|
|
1563
1573
|
}
|
|
1564
1574
|
|
|
1565
|
-
// We don't want the class exported since usage of it at runtime is an
|
|
1566
|
-
// implementation detail, but we do want to export the shape so folks can
|
|
1567
|
-
// build their own abstractions around instances via isRouteErrorResponse()
|
|
1568
|
-
export type ErrorResponse = InstanceType<typeof ErrorResponseImpl>;
|
|
1569
|
-
|
|
1570
1575
|
/**
|
|
1571
1576
|
* Check if the given error is an ErrorResponse generated from a 4xx/5xx
|
|
1572
1577
|
* Response thrown from an action/loader
|