@remix-run/router 1.3.0-pre.1 → 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 +6 -0
- package/dist/history.d.ts +4 -0
- package/dist/router.cjs.js +248 -28
- package/dist/router.cjs.js.map +1 -1
- package/dist/router.d.ts +48 -2
- package/dist/router.js +248 -29
- package/dist/router.js.map +1 -1
- package/dist/router.umd.js +248 -28
- 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 +253 -9
- 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
|
/**
|
|
@@ -461,6 +486,35 @@ type FetcherStates<TData = any> = {
|
|
|
461
486
|
export type Fetcher<TData = any> =
|
|
462
487
|
FetcherStates<TData>[keyof FetcherStates<TData>];
|
|
463
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
|
+
|
|
464
518
|
interface ShortCircuitable {
|
|
465
519
|
/**
|
|
466
520
|
* startNavigation does not need to complete the navigation because we
|
|
@@ -562,6 +616,13 @@ export const IDLE_FETCHER: FetcherStates["Idle"] = {
|
|
|
562
616
|
formData: undefined,
|
|
563
617
|
};
|
|
564
618
|
|
|
619
|
+
export const IDLE_BLOCKER: BlockerUnblocked = {
|
|
620
|
+
state: "unblocked",
|
|
621
|
+
proceed: undefined,
|
|
622
|
+
reset: undefined,
|
|
623
|
+
location: undefined,
|
|
624
|
+
};
|
|
625
|
+
|
|
565
626
|
const isBrowser =
|
|
566
627
|
typeof window !== "undefined" &&
|
|
567
628
|
typeof window.document !== "undefined" &&
|
|
@@ -637,50 +698,76 @@ export function createRouter(init: RouterInit): Router {
|
|
|
637
698
|
actionData: (init.hydrationData && init.hydrationData.actionData) || null,
|
|
638
699
|
errors: (init.hydrationData && init.hydrationData.errors) || initialErrors,
|
|
639
700
|
fetchers: new Map(),
|
|
701
|
+
blockers: new Map(),
|
|
640
702
|
};
|
|
641
703
|
|
|
642
704
|
// -- Stateful internal variables to manage navigations --
|
|
643
705
|
// Current navigation in progress (to be committed in completeNavigation)
|
|
644
706
|
let pendingAction: HistoryAction = HistoryAction.Pop;
|
|
707
|
+
|
|
645
708
|
// Should the current navigation prevent the scroll reset if scroll cannot
|
|
646
709
|
// be restored?
|
|
647
710
|
let pendingPreventScrollReset = false;
|
|
711
|
+
|
|
648
712
|
// AbortController for the active navigation
|
|
649
713
|
let pendingNavigationController: AbortController | null;
|
|
714
|
+
|
|
650
715
|
// We use this to avoid touching history in completeNavigation if a
|
|
651
716
|
// revalidation is entirely uninterrupted
|
|
652
717
|
let isUninterruptedRevalidation = false;
|
|
718
|
+
|
|
653
719
|
// Use this internal flag to force revalidation of all loaders:
|
|
654
720
|
// - submissions (completed or interrupted)
|
|
655
721
|
// - useRevalidate()
|
|
656
722
|
// - X-Remix-Revalidate (from redirect)
|
|
657
723
|
let isRevalidationRequired = false;
|
|
724
|
+
|
|
658
725
|
// Use this internal array to capture routes that require revalidation due
|
|
659
726
|
// to a cancelled deferred on action submission
|
|
660
727
|
let cancelledDeferredRoutes: string[] = [];
|
|
728
|
+
|
|
661
729
|
// Use this internal array to capture fetcher loads that were cancelled by an
|
|
662
730
|
// action navigation and require revalidation
|
|
663
731
|
let cancelledFetcherLoads: string[] = [];
|
|
732
|
+
|
|
664
733
|
// AbortControllers for any in-flight fetchers
|
|
665
734
|
let fetchControllers = new Map<string, AbortController>();
|
|
735
|
+
|
|
666
736
|
// Track loads based on the order in which they started
|
|
667
737
|
let incrementingLoadId = 0;
|
|
738
|
+
|
|
668
739
|
// Track the outstanding pending navigation data load to be compared against
|
|
669
740
|
// the globally incrementing load when a fetcher load lands after a completed
|
|
670
741
|
// navigation
|
|
671
742
|
let pendingNavigationLoadId = -1;
|
|
743
|
+
|
|
672
744
|
// Fetchers that triggered data reloads as a result of their actions
|
|
673
745
|
let fetchReloadIds = new Map<string, number>();
|
|
746
|
+
|
|
674
747
|
// Fetchers that triggered redirect navigations from their actions
|
|
675
748
|
let fetchRedirectIds = new Set<string>();
|
|
749
|
+
|
|
676
750
|
// Most recent href/match for fetcher.load calls for fetchers
|
|
677
751
|
let fetchLoadMatches = new Map<string, FetchLoadMatch>();
|
|
752
|
+
|
|
678
753
|
// Store DeferredData instances for active route matches. When a
|
|
679
754
|
// route loader returns defer() we stick one in here. Then, when a nested
|
|
680
755
|
// promise resolves we update loaderData. If a new navigation starts we
|
|
681
756
|
// cancel active deferreds for eliminated routes.
|
|
682
757
|
let activeDeferreds = new Map<string, DeferredData>();
|
|
683
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
|
+
|
|
684
771
|
// Initialize the router, all side effects should be kicked off from here.
|
|
685
772
|
// Implemented as a Fluent API for ease of:
|
|
686
773
|
// let router = createRouter(init).initialize();
|
|
@@ -688,8 +775,48 @@ export function createRouter(init: RouterInit): Router {
|
|
|
688
775
|
// If history informs us of a POP navigation, start the navigation but do not update
|
|
689
776
|
// state. We'll update our own state once the navigation completes
|
|
690
777
|
unlistenHistory = init.history.listen(
|
|
691
|
-
({ action: historyAction, location }) =>
|
|
692
|
-
|
|
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
|
+
}
|
|
693
820
|
);
|
|
694
821
|
|
|
695
822
|
// Kick off initial data load if needed. Use Pop to avoid modifying history
|
|
@@ -708,6 +835,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
708
835
|
subscribers.clear();
|
|
709
836
|
pendingNavigationController && pendingNavigationController.abort();
|
|
710
837
|
state.fetchers.forEach((_, key) => deleteFetcher(key));
|
|
838
|
+
state.blockers.forEach((_, key) => deleteBlocker(key));
|
|
711
839
|
}
|
|
712
840
|
|
|
713
841
|
// Subscribe to state updates for the router
|
|
@@ -772,6 +900,12 @@ export function createRouter(init: RouterInit): Router {
|
|
|
772
900
|
)
|
|
773
901
|
: state.loaderData;
|
|
774
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
|
+
|
|
775
909
|
// Always respect the user flag. Otherwise don't reset on mutation
|
|
776
910
|
// submission navigations unless they redirect
|
|
777
911
|
let preventScrollReset =
|
|
@@ -794,6 +928,7 @@ export function createRouter(init: RouterInit): Router {
|
|
|
794
928
|
newState.matches || state.matches
|
|
795
929
|
),
|
|
796
930
|
preventScrollReset,
|
|
931
|
+
blockers: new Map(state.blockers),
|
|
797
932
|
});
|
|
798
933
|
|
|
799
934
|
if (isUninterruptedRevalidation) {
|
|
@@ -828,16 +963,17 @@ export function createRouter(init: RouterInit): Router {
|
|
|
828
963
|
|
|
829
964
|
let { path, submission, error } = normalizeNavigateOptions(to, opts);
|
|
830
965
|
|
|
831
|
-
let
|
|
966
|
+
let currentLocation = state.location;
|
|
967
|
+
let nextLocation = createLocation(state.location, path, opts && opts.state);
|
|
832
968
|
|
|
833
969
|
// When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
|
|
834
970
|
// URL from window.location, so we need to encode it here so the behavior
|
|
835
971
|
// remains the same as POP and non-data-router usages. new URL() does all
|
|
836
972
|
// the same encoding we'd get from a history.pushState/window.location read
|
|
837
973
|
// without having to touch history
|
|
838
|
-
|
|
839
|
-
...
|
|
840
|
-
...init.history.encodeLocation(
|
|
974
|
+
nextLocation = {
|
|
975
|
+
...nextLocation,
|
|
976
|
+
...init.history.encodeLocation(nextLocation),
|
|
841
977
|
};
|
|
842
978
|
|
|
843
979
|
let userReplace = opts && opts.replace != null ? opts.replace : undefined;
|
|
@@ -865,7 +1001,35 @@ export function createRouter(init: RouterInit): Router {
|
|
|
865
1001
|
? opts.preventScrollReset === true
|
|
866
1002
|
: undefined;
|
|
867
1003
|
|
|
868
|
-
|
|
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, {
|
|
869
1033
|
submission,
|
|
870
1034
|
// Send through the formData serialization error if we have one so we can
|
|
871
1035
|
// render at the right error boundary after we match routes
|
|
@@ -1939,6 +2103,84 @@ export function createRouter(init: RouterInit): Router {
|
|
|
1939
2103
|
return yeetedKeys.length > 0;
|
|
1940
2104
|
}
|
|
1941
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
|
+
|
|
1942
2184
|
function cancelActiveDeferreds(
|
|
1943
2185
|
predicate?: (routeId: string) => boolean
|
|
1944
2186
|
): string[] {
|
|
@@ -2038,6 +2280,8 @@ export function createRouter(init: RouterInit): Router {
|
|
|
2038
2280
|
getFetcher,
|
|
2039
2281
|
deleteFetcher,
|
|
2040
2282
|
dispose,
|
|
2283
|
+
getBlocker,
|
|
2284
|
+
deleteBlocker,
|
|
2041
2285
|
_internalFetchControllers: fetchControllers,
|
|
2042
2286
|
_internalActiveDeferreds: activeDeferreds,
|
|
2043
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
|