@remix-run/router 1.3.0-pre.0 → 1.3.0-pre.2
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 +12 -0
- package/dist/history.d.ts +4 -0
- package/dist/router.cjs.js +259 -33
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +49 -2
- package/dist/router.js +259 -34
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +259 -33
- 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/history.ts +56 -12
- package/package.json +1 -1
- package/router.ts +271 -14
- package/utils.ts +1 -1
package/history.ts
CHANGED
|
@@ -81,6 +81,11 @@ export interface Update {
|
|
|
81
81
|
* The new location.
|
|
82
82
|
*/
|
|
83
83
|
location: Location;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The delta between this location and the former location in the history stack
|
|
87
|
+
*/
|
|
88
|
+
delta: number;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
/**
|
|
@@ -181,6 +186,7 @@ export interface History {
|
|
|
181
186
|
type HistoryState = {
|
|
182
187
|
usr: any;
|
|
183
188
|
key?: string;
|
|
189
|
+
idx: number;
|
|
184
190
|
};
|
|
185
191
|
|
|
186
192
|
const PopStateEventType = "popstate";
|
|
@@ -294,7 +300,7 @@ export function createMemoryHistory(
|
|
|
294
300
|
index += 1;
|
|
295
301
|
entries.splice(index, entries.length, nextLocation);
|
|
296
302
|
if (v5Compat && listener) {
|
|
297
|
-
listener({ action, location: nextLocation });
|
|
303
|
+
listener({ action, location: nextLocation, delta: 1 });
|
|
298
304
|
}
|
|
299
305
|
},
|
|
300
306
|
replace(to, state) {
|
|
@@ -302,14 +308,16 @@ export function createMemoryHistory(
|
|
|
302
308
|
let nextLocation = createMemoryLocation(to, state);
|
|
303
309
|
entries[index] = nextLocation;
|
|
304
310
|
if (v5Compat && listener) {
|
|
305
|
-
listener({ action, location: nextLocation });
|
|
311
|
+
listener({ action, location: nextLocation, delta: 0 });
|
|
306
312
|
}
|
|
307
313
|
},
|
|
308
314
|
go(delta) {
|
|
309
315
|
action = Action.Pop;
|
|
310
|
-
|
|
316
|
+
let nextIndex = clampIndex(index + delta);
|
|
317
|
+
let nextLocation = entries[nextIndex];
|
|
318
|
+
index = nextIndex;
|
|
311
319
|
if (listener) {
|
|
312
|
-
listener({ action, location:
|
|
320
|
+
listener({ action, location: nextLocation, delta });
|
|
313
321
|
}
|
|
314
322
|
},
|
|
315
323
|
listen(fn: Listener) {
|
|
@@ -497,10 +505,11 @@ function createKey() {
|
|
|
497
505
|
/**
|
|
498
506
|
* For browser-based histories, we combine the state and key into an object
|
|
499
507
|
*/
|
|
500
|
-
function getHistoryState(location: Location): HistoryState {
|
|
508
|
+
function getHistoryState(location: Location, index: number): HistoryState {
|
|
501
509
|
return {
|
|
502
510
|
usr: location.state,
|
|
503
511
|
key: location.key,
|
|
512
|
+
idx: index,
|
|
504
513
|
};
|
|
505
514
|
}
|
|
506
515
|
|
|
@@ -588,10 +597,43 @@ function getUrlBasedHistory(
|
|
|
588
597
|
let action = Action.Pop;
|
|
589
598
|
let listener: Listener | null = null;
|
|
590
599
|
|
|
600
|
+
let index = getIndex()!;
|
|
601
|
+
// Index should only be null when we initialize. If not, it's because the
|
|
602
|
+
// user called history.pushState or history.replaceState directly, in which
|
|
603
|
+
// case we should log a warning as it will result in bugs.
|
|
604
|
+
if (index == null) {
|
|
605
|
+
index = 0;
|
|
606
|
+
globalHistory.replaceState({ ...globalHistory.state, idx: index }, "");
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function getIndex(): number {
|
|
610
|
+
let state = globalHistory.state || { idx: null };
|
|
611
|
+
return state.idx;
|
|
612
|
+
}
|
|
613
|
+
|
|
591
614
|
function handlePop() {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
615
|
+
let nextAction = Action.Pop;
|
|
616
|
+
let nextIndex = getIndex();
|
|
617
|
+
|
|
618
|
+
if (nextIndex != null) {
|
|
619
|
+
let delta = nextIndex - index;
|
|
620
|
+
action = nextAction;
|
|
621
|
+
index = nextIndex;
|
|
622
|
+
if (listener) {
|
|
623
|
+
listener({ action, location: history.location, delta });
|
|
624
|
+
}
|
|
625
|
+
} else {
|
|
626
|
+
warning(
|
|
627
|
+
false,
|
|
628
|
+
// TODO: Write up a doc that explains our blocking strategy in detail
|
|
629
|
+
// and link to it here so people can understand better what is going on
|
|
630
|
+
// and how to avoid it.
|
|
631
|
+
`You are trying to block a POP navigation to a location that was not ` +
|
|
632
|
+
`created by @remix-run/router. The block will fail silently in ` +
|
|
633
|
+
`production, but in general you should do all navigation with the ` +
|
|
634
|
+
`router (instead of using window.history.pushState directly) ` +
|
|
635
|
+
`to avoid this situation.`
|
|
636
|
+
);
|
|
595
637
|
}
|
|
596
638
|
}
|
|
597
639
|
|
|
@@ -600,7 +642,8 @@ function getUrlBasedHistory(
|
|
|
600
642
|
let location = createLocation(history.location, to, state);
|
|
601
643
|
if (validateLocation) validateLocation(location, to);
|
|
602
644
|
|
|
603
|
-
|
|
645
|
+
index = getIndex() + 1;
|
|
646
|
+
let historyState = getHistoryState(location, index);
|
|
604
647
|
let url = history.createHref(location);
|
|
605
648
|
|
|
606
649
|
// try...catch because iOS limits us to 100 pushState calls :/
|
|
@@ -613,7 +656,7 @@ function getUrlBasedHistory(
|
|
|
613
656
|
}
|
|
614
657
|
|
|
615
658
|
if (v5Compat && listener) {
|
|
616
|
-
listener({ action, location: history.location });
|
|
659
|
+
listener({ action, location: history.location, delta: 1 });
|
|
617
660
|
}
|
|
618
661
|
}
|
|
619
662
|
|
|
@@ -622,12 +665,13 @@ function getUrlBasedHistory(
|
|
|
622
665
|
let location = createLocation(history.location, to, state);
|
|
623
666
|
if (validateLocation) validateLocation(location, to);
|
|
624
667
|
|
|
625
|
-
|
|
668
|
+
index = getIndex();
|
|
669
|
+
let historyState = getHistoryState(location, index);
|
|
626
670
|
let url = history.createHref(location);
|
|
627
671
|
globalHistory.replaceState(historyState, "", url);
|
|
628
672
|
|
|
629
673
|
if (v5Compat && listener) {
|
|
630
|
-
listener({ action, location: history.location });
|
|
674
|
+
listener({ action, location: history.location, delta: 0 });
|
|
631
675
|
}
|
|
632
676
|
}
|
|
633
677
|
|
package/package.json
CHANGED
package/router.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
joinPaths,
|
|
33
33
|
matchRoutes,
|
|
34
34
|
resolveTo,
|
|
35
|
+
warning,
|
|
35
36
|
} from "./utils";
|
|
36
37
|
|
|
37
38
|
////////////////////////////////////////////////////////////////////////////////
|
|
@@ -110,14 +111,14 @@ export interface Router {
|
|
|
110
111
|
* Navigate forward/backward in the history stack
|
|
111
112
|
* @param to Delta to move in the history stack
|
|
112
113
|
*/
|
|
113
|
-
navigate(to: number): void
|
|
114
|
+
navigate(to: number): Promise<void>;
|
|
114
115
|
|
|
115
116
|
/**
|
|
116
117
|
* Navigate to the given path
|
|
117
118
|
* @param to Path to navigate to
|
|
118
119
|
* @param opts Navigation options (method, submission, etc.)
|
|
119
120
|
*/
|
|
120
|
-
navigate(to: To, opts?: RouterNavigateOptions): void
|
|
121
|
+
navigate(to: To, opts?: RouterNavigateOptions): Promise<void>;
|
|
121
122
|
|
|
122
123
|
/**
|
|
123
124
|
* @internal
|
|
@@ -190,6 +191,25 @@ export interface Router {
|
|
|
190
191
|
*/
|
|
191
192
|
dispose(): void;
|
|
192
193
|
|
|
194
|
+
/**
|
|
195
|
+
* @internal
|
|
196
|
+
* PRIVATE - DO NOT USE
|
|
197
|
+
*
|
|
198
|
+
* Get a navigation blocker
|
|
199
|
+
* @param key The identifier for the blocker
|
|
200
|
+
* @param fn The blocker function implementation
|
|
201
|
+
*/
|
|
202
|
+
getBlocker(key: string, fn: BlockerFunction): Blocker;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @internal
|
|
206
|
+
* PRIVATE - DO NOT USE
|
|
207
|
+
*
|
|
208
|
+
* Delete a navigation blocker
|
|
209
|
+
* @param key The identifier for the blocker
|
|
210
|
+
*/
|
|
211
|
+
deleteBlocker(key: string): void;
|
|
212
|
+
|
|
193
213
|
/**
|
|
194
214
|
* @internal
|
|
195
215
|
* PRIVATE - DO NOT USE
|
|
@@ -275,6 +295,11 @@ export interface RouterState {
|
|
|
275
295
|
* Map of current fetchers
|
|
276
296
|
*/
|
|
277
297
|
fetchers: Map<string, Fetcher>;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Map of current blockers
|
|
301
|
+
*/
|
|
302
|
+
blockers: Map<string, Blocker>;
|
|
278
303
|
}
|
|
279
304
|
|
|
280
305
|
/**
|
|
@@ -371,6 +396,7 @@ type LinkNavigateOptions = {
|
|
|
371
396
|
type SubmissionNavigateOptions = {
|
|
372
397
|
replace?: boolean;
|
|
373
398
|
state?: any;
|
|
399
|
+
preventScrollReset?: boolean;
|
|
374
400
|
formMethod?: FormMethod;
|
|
375
401
|
formEncType?: FormEncType;
|
|
376
402
|
formData: FormData;
|
|
@@ -460,6 +486,35 @@ type FetcherStates<TData = any> = {
|
|
|
460
486
|
export type Fetcher<TData = any> =
|
|
461
487
|
FetcherStates<TData>[keyof FetcherStates<TData>];
|
|
462
488
|
|
|
489
|
+
interface BlockerBlocked {
|
|
490
|
+
state: "blocked";
|
|
491
|
+
reset(): void;
|
|
492
|
+
proceed(): void;
|
|
493
|
+
location: Location;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
interface BlockerUnblocked {
|
|
497
|
+
state: "unblocked";
|
|
498
|
+
reset: undefined;
|
|
499
|
+
proceed: undefined;
|
|
500
|
+
location: undefined;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
interface BlockerProceeding {
|
|
504
|
+
state: "proceeding";
|
|
505
|
+
reset: undefined;
|
|
506
|
+
proceed: undefined;
|
|
507
|
+
location: Location;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
export type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;
|
|
511
|
+
|
|
512
|
+
export type BlockerFunction = (args: {
|
|
513
|
+
currentLocation: Location;
|
|
514
|
+
nextLocation: Location;
|
|
515
|
+
historyAction: HistoryAction;
|
|
516
|
+
}) => boolean;
|
|
517
|
+
|
|
463
518
|
interface ShortCircuitable {
|
|
464
519
|
/**
|
|
465
520
|
* startNavigation does not need to complete the navigation because we
|
|
@@ -561,6 +616,13 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
|
|
|
561
616
|
formData: undefined,
|
|
562
617
|
};
|
|
563
618
|
|
|
619
|
+
export const IDLE_BLOCKER: BlockerUnblocked = {
|
|
620
|
+
state: "unblocked",
|
|
621
|
+
proceed: undefined,
|
|
622
|
+
reset: undefined,
|
|
623
|
+
location: undefined,
|
|
624
|
+
};
|
|
625
|
+
|
|
564
626
|
const isBrowser =
|
|
565
627
|
typeof window !== "undefined" &&
|
|
566
628
|
typeof window.document !== "undefined" &&
|
|
@@ -636,50 +698,76 @@ export function createRouter(init: RouterInit): Router {
|
|
|
636
698
|
actionData: (init.hydrationData && init.hydrationData.actionData) || null,
|
|
637
699
|
errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
|
|
638
700
|
fetchers: new Map(),
|
|
701
|
+
blockers: new Map(),
|
|
639
702
|
};
|
|
640
703
|
|
|
641
704
|
// -- Stateful internal variables to manage navigations --
|
|
642
705
|
// Current navigation in progress (to be committed in completeNavigation)
|
|
643
706
|
let pendingAction: HistoryAction = HistoryAction.Pop;
|
|
707
|
+
|
|
644
708
|
// Should the current navigation prevent the scroll reset if scroll cannot
|
|
645
709
|
// be restored?
|
|
646
710
|
let pendingPreventScrollReset = false;
|
|
711
|
+
|
|
647
712
|
// AbortController for the active navigation
|
|
648
713
|
let pendingNavigationController: AbortController | null;
|
|
714
|
+
|
|
649
715
|
// We use this to avoid touching history in completeNavigation if a
|
|
650
716
|
// revalidation is entirely uninterrupted
|
|
651
717
|
let isUninterruptedRevalidation = false;
|
|
718
|
+
|
|
652
719
|
// Use this internal flag to force revalidation of all loaders:
|
|
653
720
|
// - submissions (completed or interrupted)
|
|
654
721
|
// - useRevalidate()
|
|
655
722
|
// - X-Remix-Revalidate (from redirect)
|
|
656
723
|
let isRevalidationRequired = false;
|
|
724
|
+
|
|
657
725
|
// Use this internal array to capture routes that require revalidation due
|
|
658
726
|
// to a cancelled deferred on action submission
|
|
659
727
|
let cancelledDeferredRoutes: string[] = [];
|
|
728
|
+
|
|
660
729
|
// Use this internal array to capture fetcher loads that were cancelled by an
|
|
661
730
|
// action navigation and require revalidation
|
|
662
731
|
let cancelledFetcherLoads: string[] = [];
|
|
732
|
+
|
|
663
733
|
// AbortControllers for any in-flight fetchers
|
|
664
734
|
let fetchControllers = new Map<string, AbortController>();
|
|
735
|
+
|
|
665
736
|
// Track loads based on the order in which they started
|
|
666
737
|
let incrementingLoadId = 0;
|
|
738
|
+
|
|
667
739
|
// Track the outstanding pending navigation data load to be compared against
|
|
668
740
|
// the globally incrementing load when a fetcher load lands after a completed
|
|
669
741
|
// navigation
|
|
670
742
|
let pendingNavigationLoadId = -1;
|
|
743
|
+
|
|
671
744
|
// Fetchers that triggered data reloads as a result of their actions
|
|
672
745
|
let fetchReloadIds = new Map<string, number>();
|
|
746
|
+
|
|
673
747
|
// Fetchers that triggered redirect navigations from their actions
|
|
674
748
|
let fetchRedirectIds = new Set<string>();
|
|
749
|
+
|
|
675
750
|
// Most recent href/match for fetcher.load calls for fetchers
|
|
676
751
|
let fetchLoadMatches = new Map<string, FetchLoadMatch>();
|
|
752
|
+
|
|
677
753
|
// Store DeferredData instances for active route matches. When a
|
|
678
754
|
// route loader returns defer() we stick one in here. Then, when a nested
|
|
679
755
|
// promise resolves we update loaderData. If a new navigation starts we
|
|
680
756
|
// cancel active deferreds for eliminated routes.
|
|
681
757
|
let activeDeferreds = new Map<string, DeferredData>();
|
|
682
758
|
|
|
759
|
+
// We ony support a single active blocker at the moment since we don't have
|
|
760
|
+
// any compelling use cases for multi-blocker yet
|
|
761
|
+
let activeBlocker: string | null = null;
|
|
762
|
+
|
|
763
|
+
// Store blocker functions in a separate Map outside of router state since
|
|
764
|
+
// we don't need to update UI state if they change
|
|
765
|
+
let blockerFunctions = new Map<string, BlockerFunction>();
|
|
766
|
+
|
|
767
|
+
// Flag to ignore the next history update, so we can revert the URL change on
|
|
768
|
+
// a POP navigation that was blocked by the user without touching router state
|
|
769
|
+
let ignoreNextHistoryUpdate = false;
|
|
770
|
+
|
|
683
771
|
// Initialize the router, all side effects should be kicked off from here.
|
|
684
772
|
// Implemented as a Fluent API for ease of:
|
|
685
773
|
// let router = createRouter(init).initialize();
|
|
@@ -687,8 +775,48 @@ export function createRouter(init: RouterInit): Router {
|
|
|
687
775
|
// If history informs us of a POP navigation, start the navigation but do not update
|
|
688
776
|
// state. We'll update our own state once the navigation completes
|
|
689
777
|
unlistenHistory = init.history.listen(
|
|
690
|
-
({ action: historyAction, location }) =>
|
|
691
|
-
|
|
778
|
+
({ action: historyAction, location, delta }) => {
|
|
779
|
+
// Ignore this event if it was just us resetting the URL from a
|
|
780
|
+
// blocked POP navigation
|
|
781
|
+
if (ignoreNextHistoryUpdate) {
|
|
782
|
+
ignoreNextHistoryUpdate = false;
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
let blockerKey = shouldBlockNavigation({
|
|
787
|
+
currentLocation: state.location,
|
|
788
|
+
nextLocation: location,
|
|
789
|
+
historyAction,
|
|
790
|
+
});
|
|
791
|
+
if (blockerKey) {
|
|
792
|
+
// Restore the URL to match the current UI, but don't update router state
|
|
793
|
+
ignoreNextHistoryUpdate = true;
|
|
794
|
+
init.history.go(delta * -1);
|
|
795
|
+
|
|
796
|
+
// Put the blocker into a blocked state
|
|
797
|
+
updateBlocker(blockerKey, {
|
|
798
|
+
state: "blocked",
|
|
799
|
+
location,
|
|
800
|
+
proceed() {
|
|
801
|
+
updateBlocker(blockerKey!, {
|
|
802
|
+
state: "proceeding",
|
|
803
|
+
proceed: undefined,
|
|
804
|
+
reset: undefined,
|
|
805
|
+
location,
|
|
806
|
+
});
|
|
807
|
+
// Re-do the same POP navigation we just blocked
|
|
808
|
+
init.history.go(delta);
|
|
809
|
+
},
|
|
810
|
+
reset() {
|
|
811
|
+
deleteBlocker(blockerKey!);
|
|
812
|
+
updateState({ blockers: new Map(router.state.blockers) });
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return startNavigation(historyAction, location);
|
|
819
|
+
}
|
|
692
820
|
);
|
|
693
821
|
|
|
694
822
|
// Kick off initial data load if needed. Use Pop to avoid modifying history
|
|
@@ -707,6 +835,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
707
835
|
subscribers.clear();
|
|
708
836
|
pendingNavigationController && pendingNavigationController.abort();
|
|
709
837
|
state.fetchers.forEach((_, key) => deleteFetcher(key));
|
|
838
|
+
state.blockers.forEach((_, key) => deleteBlocker(key));
|
|
710
839
|
}
|
|
711
840
|
|
|
712
841
|
// Subscribe to state updates for the router
|
|
@@ -771,6 +900,20 @@ export function createRouter(init: RouterInit): Router {
|
|
|
771
900
|
)
|
|
772
901
|
: state.loaderData;
|
|
773
902
|
|
|
903
|
+
// On a successful navigation we can assume we got through all blockers
|
|
904
|
+
// so we can start fresh
|
|
905
|
+
for (let [key] of blockerFunctions) {
|
|
906
|
+
deleteBlocker(key);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Always respect the user flag. Otherwise don't reset on mutation
|
|
910
|
+
// submission navigations unless they redirect
|
|
911
|
+
let preventScrollReset =
|
|
912
|
+
pendingPreventScrollReset === true ||
|
|
913
|
+
(state.navigation.formMethod != null &&
|
|
914
|
+
isMutationMethod(state.navigation.formMethod) &&
|
|
915
|
+
location.state?._isRedirect !== true);
|
|
916
|
+
|
|
774
917
|
updateState({
|
|
775
918
|
...newState, // matches, errors, fetchers go through as-is
|
|
776
919
|
actionData,
|
|
@@ -780,11 +923,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
780
923
|
initialized: true,
|
|
781
924
|
navigation: IDLE_NAVIGATION,
|
|
782
925
|
revalidation: "idle",
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
preventScrollReset
|
|
926
|
+
restoreScrollPosition: getSavedScrollPosition(
|
|
927
|
+
location,
|
|
928
|
+
newState.matches || state.matches
|
|
929
|
+
),
|
|
930
|
+
preventScrollReset,
|
|
931
|
+
blockers: new Map(state.blockers),
|
|
788
932
|
});
|
|
789
933
|
|
|
790
934
|
if (isUninterruptedRevalidation) {
|
|
@@ -819,16 +963,17 @@ export function createRouter(init: RouterInit): Router {
|
|
|
819
963
|
|
|
820
964
|
let { path, submission, error } = normalizeNavigateOptions(to, opts);
|
|
821
965
|
|
|
822
|
-
let
|
|
966
|
+
let currentLocation = state.location;
|
|
967
|
+
let nextLocation = createLocation(state.location, path, opts && opts.state);
|
|
823
968
|
|
|
824
969
|
// When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
|
|
825
970
|
// URL from window.location, so we need to encode it here so the behavior
|
|
826
971
|
// remains the same as POP and non-data-router usages. new URL() does all
|
|
827
972
|
// the same encoding we'd get from a history.pushState/window.location read
|
|
828
973
|
// without having to touch history
|
|
829
|
-
|
|
830
|
-
...
|
|
831
|
-
...init.history.encodeLocation(
|
|
974
|
+
nextLocation = {
|
|
975
|
+
...nextLocation,
|
|
976
|
+
...init.history.encodeLocation(nextLocation),
|
|
832
977
|
};
|
|
833
978
|
|
|
834
979
|
let userReplace = opts && opts.replace != null ? opts.replace : undefined;
|
|
@@ -856,7 +1001,35 @@ export function createRouter(init: RouterInit): Router {
|
|
|
856
1001
|
? opts.preventScrollReset === true
|
|
857
1002
|
: undefined;
|
|
858
1003
|
|
|
859
|
-
|
|
1004
|
+
let blockerKey = shouldBlockNavigation({
|
|
1005
|
+
currentLocation,
|
|
1006
|
+
nextLocation,
|
|
1007
|
+
historyAction,
|
|
1008
|
+
});
|
|
1009
|
+
if (blockerKey) {
|
|
1010
|
+
// Put the blocker into a blocked state
|
|
1011
|
+
updateBlocker(blockerKey, {
|
|
1012
|
+
state: "blocked",
|
|
1013
|
+
location: nextLocation,
|
|
1014
|
+
proceed() {
|
|
1015
|
+
updateBlocker(blockerKey!, {
|
|
1016
|
+
state: "proceeding",
|
|
1017
|
+
proceed: undefined,
|
|
1018
|
+
reset: undefined,
|
|
1019
|
+
location: nextLocation,
|
|
1020
|
+
});
|
|
1021
|
+
// Send the same navigation through
|
|
1022
|
+
navigate(to, opts);
|
|
1023
|
+
},
|
|
1024
|
+
reset() {
|
|
1025
|
+
deleteBlocker(blockerKey!);
|
|
1026
|
+
updateState({ blockers: new Map(state.blockers) });
|
|
1027
|
+
},
|
|
1028
|
+
});
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
return await startNavigation(historyAction, nextLocation, {
|
|
860
1033
|
submission,
|
|
861
1034
|
// Send through the formData serialization error if we have one so we can
|
|
862
1035
|
// render at the right error boundary after we match routes
|
|
@@ -1772,6 +1945,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1772
1945
|
...submission,
|
|
1773
1946
|
formAction: redirect.location,
|
|
1774
1947
|
},
|
|
1948
|
+
// Preserve this flag across redirects
|
|
1949
|
+
preventScrollReset: pendingPreventScrollReset,
|
|
1775
1950
|
});
|
|
1776
1951
|
} else {
|
|
1777
1952
|
// Otherwise, we kick off a new loading navigation, preserving the
|
|
@@ -1785,6 +1960,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1785
1960
|
formEncType: submission ? submission.formEncType : undefined,
|
|
1786
1961
|
formData: submission ? submission.formData : undefined,
|
|
1787
1962
|
},
|
|
1963
|
+
// Preserve this flag across redirects
|
|
1964
|
+
preventScrollReset: pendingPreventScrollReset,
|
|
1788
1965
|
});
|
|
1789
1966
|
}
|
|
1790
1967
|
}
|
|
@@ -1926,6 +2103,84 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1926
2103
|
return yeetedKeys.length > 0;
|
|
1927
2104
|
}
|
|
1928
2105
|
|
|
2106
|
+
function getBlocker(key: string, fn: BlockerFunction) {
|
|
2107
|
+
let blocker: Blocker = state.blockers.get(key) || IDLE_BLOCKER;
|
|
2108
|
+
|
|
2109
|
+
if (blockerFunctions.get(key) !== fn) {
|
|
2110
|
+
blockerFunctions.set(key, fn);
|
|
2111
|
+
if (activeBlocker == null) {
|
|
2112
|
+
// This is now the active blocker
|
|
2113
|
+
activeBlocker = key;
|
|
2114
|
+
} else if (key !== activeBlocker) {
|
|
2115
|
+
warning(false, "A router only supports one blocker at a time");
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
return blocker;
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
function deleteBlocker(key: string) {
|
|
2123
|
+
state.blockers.delete(key);
|
|
2124
|
+
blockerFunctions.delete(key);
|
|
2125
|
+
if (activeBlocker === key) {
|
|
2126
|
+
activeBlocker = null;
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Utility function to update blockers, ensuring valid state transitions
|
|
2131
|
+
function updateBlocker(key: string, newBlocker: Blocker) {
|
|
2132
|
+
let blocker = state.blockers.get(key) || IDLE_BLOCKER;
|
|
2133
|
+
|
|
2134
|
+
// Poor mans state machine :)
|
|
2135
|
+
// https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
|
|
2136
|
+
invariant(
|
|
2137
|
+
(blocker.state === "unblocked" && newBlocker.state === "blocked") ||
|
|
2138
|
+
(blocker.state === "blocked" && newBlocker.state === "blocked") ||
|
|
2139
|
+
(blocker.state === "blocked" && newBlocker.state === "proceeding") ||
|
|
2140
|
+
(blocker.state === "blocked" && newBlocker.state === "unblocked") ||
|
|
2141
|
+
(blocker.state === "proceeding" && newBlocker.state === "unblocked"),
|
|
2142
|
+
`Invalid blocker state transition: ${blocker.state} -> ${newBlocker.state}`
|
|
2143
|
+
);
|
|
2144
|
+
|
|
2145
|
+
state.blockers.set(key, newBlocker);
|
|
2146
|
+
updateState({ blockers: new Map(state.blockers) });
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
function shouldBlockNavigation({
|
|
2150
|
+
currentLocation,
|
|
2151
|
+
nextLocation,
|
|
2152
|
+
historyAction,
|
|
2153
|
+
}: {
|
|
2154
|
+
currentLocation: Location;
|
|
2155
|
+
nextLocation: Location;
|
|
2156
|
+
historyAction: HistoryAction;
|
|
2157
|
+
}): string | undefined {
|
|
2158
|
+
if (activeBlocker == null) {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// We only allow a single blocker at the moment. This will need to be
|
|
2163
|
+
// updated if we enhance to support multiple blockers in the future
|
|
2164
|
+
let blockerFunction = blockerFunctions.get(activeBlocker);
|
|
2165
|
+
invariant(
|
|
2166
|
+
blockerFunction,
|
|
2167
|
+
"Could not find a function for the active blocker"
|
|
2168
|
+
);
|
|
2169
|
+
let blocker = state.blockers.get(activeBlocker);
|
|
2170
|
+
|
|
2171
|
+
if (blocker && blocker.state === "proceeding") {
|
|
2172
|
+
// If the blocker is currently proceeding, we don't need to re-check
|
|
2173
|
+
// it and can let this navigation continue
|
|
2174
|
+
return;
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// At this point, we know we're unblocked/blocked so we need to check the
|
|
2178
|
+
// user-provided blocker function
|
|
2179
|
+
if (blockerFunction({ currentLocation, nextLocation, historyAction })) {
|
|
2180
|
+
return activeBlocker;
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1929
2184
|
function cancelActiveDeferreds(
|
|
1930
2185
|
predicate?: (routeId: string) => boolean
|
|
1931
2186
|
): string[] {
|
|
@@ -2025,6 +2280,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2025
2280
|
getFetcher,
|
|
2026
2281
|
deleteFetcher,
|
|
2027
2282
|
dispose,
|
|
2283
|
+
getBlocker,
|
|
2284
|
+
deleteBlocker,
|
|
2028
2285
|
_internalFetchControllers: fetchControllers,
|
|
2029
2286
|
_internalActiveDeferreds: activeDeferreds,
|
|
2030
2287
|
};
|
package/utils.ts
CHANGED
|
@@ -900,7 +900,7 @@ export function warning(cond: any, message: string): void {
|
|
|
900
900
|
if (typeof console !== "undefined") console.warn(message);
|
|
901
901
|
|
|
902
902
|
try {
|
|
903
|
-
// Welcome to debugging
|
|
903
|
+
// Welcome to debugging @remix-run/router!
|
|
904
904
|
//
|
|
905
905
|
// This error is thrown as a convenience so you can more easily
|
|
906
906
|
// find the source for a warning that appears in the console by
|