@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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # `@remix-run/router`
2
2
 
3
+ ## 1.3.0-pre.2
4
+
5
+ ### Minor Changes
6
+
7
+ - Added support for navigation blocking APIs ([#9709](https://github.com/remix-run/react-router/pull/9709))
8
+
9
+ ## 1.3.0-pre.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Fix scroll reset if a submission redirects ([#9886](https://github.com/remix-run/react-router/pull/9886))
14
+
3
15
  ## 1.3.0-pre.0
4
16
 
5
17
  ### Minor Changes
package/dist/history.d.ts CHANGED
@@ -68,6 +68,10 @@ export interface Update {
68
68
  * The new location.
69
69
  */
70
70
  location: Location;
71
+ /**
72
+ * The delta between this location and the former location in the history stack
73
+ */
74
+ delta: number;
71
75
  }
72
76
  /**
73
77
  * A function that receives notifications about location changes.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @remix-run/router v1.3.0-pre.0
2
+ * @remix-run/router v1.3.0-pre.2
3
3
  *
4
4
  * Copyright (c) Remix Software Inc.
5
5
  *
@@ -137,7 +137,8 @@ function createMemoryHistory(options) {
137
137
  if (v5Compat && listener) {
138
138
  listener({
139
139
  action,
140
- location: nextLocation
140
+ location: nextLocation,
141
+ delta: 1
141
142
  });
142
143
  }
143
144
  },
@@ -150,19 +151,23 @@ function createMemoryHistory(options) {
150
151
  if (v5Compat && listener) {
151
152
  listener({
152
153
  action,
153
- location: nextLocation
154
+ location: nextLocation,
155
+ delta: 0
154
156
  });
155
157
  }
156
158
  },
157
159
 
158
160
  go(delta) {
159
161
  action = exports.Action.Pop;
160
- index = clampIndex(index + delta);
162
+ let nextIndex = clampIndex(index + delta);
163
+ let nextLocation = entries[nextIndex];
164
+ index = nextIndex;
161
165
 
162
166
  if (listener) {
163
167
  listener({
164
168
  action,
165
- location: getCurrentLocation()
169
+ location: nextLocation,
170
+ delta
166
171
  });
167
172
  }
168
173
  },
@@ -321,10 +326,11 @@ function createKey() {
321
326
  */
322
327
 
323
328
 
324
- function getHistoryState(location) {
329
+ function getHistoryState(location, index) {
325
330
  return {
326
331
  usr: location.state,
327
- key: location.key
332
+ key: location.key,
333
+ idx: index
328
334
  };
329
335
  }
330
336
  /**
@@ -408,15 +414,45 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
408
414
  let globalHistory = window.history;
409
415
  let action = exports.Action.Pop;
410
416
  let listener = null;
417
+ let index = getIndex(); // Index should only be null when we initialize. If not, it's because the
418
+ // user called history.pushState or history.replaceState directly, in which
419
+ // case we should log a warning as it will result in bugs.
420
+
421
+ if (index == null) {
422
+ index = 0;
423
+ globalHistory.replaceState(_extends({}, globalHistory.state, {
424
+ idx: index
425
+ }), "");
426
+ }
427
+
428
+ function getIndex() {
429
+ let state = globalHistory.state || {
430
+ idx: null
431
+ };
432
+ return state.idx;
433
+ }
411
434
 
412
435
  function handlePop() {
413
- action = exports.Action.Pop;
436
+ let nextAction = exports.Action.Pop;
437
+ let nextIndex = getIndex();
414
438
 
415
- if (listener) {
416
- listener({
417
- action,
418
- location: history.location
419
- });
439
+ if (nextIndex != null) {
440
+ let delta = nextIndex - index;
441
+ action = nextAction;
442
+ index = nextIndex;
443
+
444
+ if (listener) {
445
+ listener({
446
+ action,
447
+ location: history.location,
448
+ delta
449
+ });
450
+ }
451
+ } else {
452
+ warning$1(false, // TODO: Write up a doc that explains our blocking strategy in detail
453
+ // and link to it here so people can understand better what is going on
454
+ // and how to avoid it.
455
+ "You are trying to block a POP navigation to a location that was not " + "created by @remix-run/router. The block will fail silently in " + "production, but in general you should do all navigation with the " + "router (instead of using window.history.pushState directly) " + "to avoid this situation.");
420
456
  }
421
457
  }
422
458
 
@@ -424,7 +460,8 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
424
460
  action = exports.Action.Push;
425
461
  let location = createLocation(history.location, to, state);
426
462
  if (validateLocation) validateLocation(location, to);
427
- let historyState = getHistoryState(location);
463
+ index = getIndex() + 1;
464
+ let historyState = getHistoryState(location, index);
428
465
  let url = history.createHref(location); // try...catch because iOS limits us to 100 pushState calls :/
429
466
 
430
467
  try {
@@ -438,7 +475,8 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
438
475
  if (v5Compat && listener) {
439
476
  listener({
440
477
  action,
441
- location: history.location
478
+ location: history.location,
479
+ delta: 1
442
480
  });
443
481
  }
444
482
  }
@@ -447,14 +485,16 @@ function getUrlBasedHistory(getLocation, createHref, validateLocation, options)
447
485
  action = exports.Action.Replace;
448
486
  let location = createLocation(history.location, to, state);
449
487
  if (validateLocation) validateLocation(location, to);
450
- let historyState = getHistoryState(location);
488
+ index = getIndex();
489
+ let historyState = getHistoryState(location, index);
451
490
  let url = history.createHref(location);
452
491
  globalHistory.replaceState(historyState, "", url);
453
492
 
454
493
  if (v5Compat && listener) {
455
494
  listener({
456
495
  action,
457
- location: history.location
496
+ location: history.location,
497
+ delta: 0
458
498
  });
459
499
  }
460
500
  }
@@ -990,7 +1030,7 @@ function warning(cond, message) {
990
1030
  if (typeof console !== "undefined") console.warn(message);
991
1031
 
992
1032
  try {
993
- // Welcome to debugging React Router!
1033
+ // Welcome to debugging @remix-run/router!
994
1034
  //
995
1035
  // This error is thrown as a convenience so you can more easily
996
1036
  // find the source for a warning that appears in the console by
@@ -1433,6 +1473,12 @@ const IDLE_FETCHER = {
1433
1473
  formEncType: undefined,
1434
1474
  formData: undefined
1435
1475
  };
1476
+ const IDLE_BLOCKER = {
1477
+ state: "unblocked",
1478
+ proceed: undefined,
1479
+ reset: undefined,
1480
+ location: undefined
1481
+ };
1436
1482
  const isBrowser = typeof window !== "undefined" && typeof window.document !== "undefined" && typeof window.document.createElement !== "undefined";
1437
1483
  const isServer = !isBrowser; //#endregion
1438
1484
  ////////////////////////////////////////////////////////////////////////////////
@@ -1497,7 +1543,8 @@ function createRouter(init) {
1497
1543
  loaderData: init.hydrationData && init.hydrationData.loaderData || {},
1498
1544
  actionData: init.hydrationData && init.hydrationData.actionData || null,
1499
1545
  errors: init.hydrationData && init.hydrationData.errors || initialErrors,
1500
- fetchers: new Map()
1546
+ fetchers: new Map(),
1547
+ blockers: new Map()
1501
1548
  }; // -- Stateful internal variables to manage navigations --
1502
1549
  // Current navigation in progress (to be committed in completeNavigation)
1503
1550
 
@@ -1539,7 +1586,16 @@ function createRouter(init) {
1539
1586
  // promise resolves we update loaderData. If a new navigation starts we
1540
1587
  // cancel active deferreds for eliminated routes.
1541
1588
 
1542
- let activeDeferreds = new Map(); // Initialize the router, all side effects should be kicked off from here.
1589
+ let activeDeferreds = new Map(); // We ony support a single active blocker at the moment since we don't have
1590
+ // any compelling use cases for multi-blocker yet
1591
+
1592
+ let activeBlocker = null; // Store blocker functions in a separate Map outside of router state since
1593
+ // we don't need to update UI state if they change
1594
+
1595
+ let blockerFunctions = new Map(); // Flag to ignore the next history update, so we can revert the URL change on
1596
+ // a POP navigation that was blocked by the user without touching router state
1597
+
1598
+ let ignoreNextHistoryUpdate = false; // Initialize the router, all side effects should be kicked off from here.
1543
1599
  // Implemented as a Fluent API for ease of:
1544
1600
  // let router = createRouter(init).initialize();
1545
1601
 
@@ -1549,8 +1605,54 @@ function createRouter(init) {
1549
1605
  unlistenHistory = init.history.listen(_ref => {
1550
1606
  let {
1551
1607
  action: historyAction,
1552
- location
1608
+ location,
1609
+ delta
1553
1610
  } = _ref;
1611
+
1612
+ // Ignore this event if it was just us resetting the URL from a
1613
+ // blocked POP navigation
1614
+ if (ignoreNextHistoryUpdate) {
1615
+ ignoreNextHistoryUpdate = false;
1616
+ return;
1617
+ }
1618
+
1619
+ let blockerKey = shouldBlockNavigation({
1620
+ currentLocation: state.location,
1621
+ nextLocation: location,
1622
+ historyAction
1623
+ });
1624
+
1625
+ if (blockerKey) {
1626
+ // Restore the URL to match the current UI, but don't update router state
1627
+ ignoreNextHistoryUpdate = true;
1628
+ init.history.go(delta * -1); // Put the blocker into a blocked state
1629
+
1630
+ updateBlocker(blockerKey, {
1631
+ state: "blocked",
1632
+ location,
1633
+
1634
+ proceed() {
1635
+ updateBlocker(blockerKey, {
1636
+ state: "proceeding",
1637
+ proceed: undefined,
1638
+ reset: undefined,
1639
+ location
1640
+ }); // Re-do the same POP navigation we just blocked
1641
+
1642
+ init.history.go(delta);
1643
+ },
1644
+
1645
+ reset() {
1646
+ deleteBlocker(blockerKey);
1647
+ updateState({
1648
+ blockers: new Map(router.state.blockers)
1649
+ });
1650
+ }
1651
+
1652
+ });
1653
+ return;
1654
+ }
1655
+
1554
1656
  return startNavigation(historyAction, location);
1555
1657
  }); // Kick off initial data load if needed. Use Pop to avoid modifying history
1556
1658
 
@@ -1570,6 +1672,7 @@ function createRouter(init) {
1570
1672
  subscribers.clear();
1571
1673
  pendingNavigationController && pendingNavigationController.abort();
1572
1674
  state.fetchers.forEach((_, key) => deleteFetcher(key));
1675
+ state.blockers.forEach((_, key) => deleteBlocker(key));
1573
1676
  } // Subscribe to state updates for the router
1574
1677
 
1575
1678
 
@@ -1590,7 +1693,7 @@ function createRouter(init) {
1590
1693
 
1591
1694
 
1592
1695
  function completeNavigation(location, newState) {
1593
- var _location$state;
1696
+ var _location$state, _location$state2;
1594
1697
 
1595
1698
  // Deduce if we're in a loading/actionReload state:
1596
1699
  // - We have committed actionData in the store
@@ -1616,7 +1719,16 @@ function createRouter(init) {
1616
1719
  } // Always preserve any existing loaderData from re-used routes
1617
1720
 
1618
1721
 
1619
- let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData;
1722
+ let loaderData = newState.loaderData ? mergeLoaderData(state.loaderData, newState.loaderData, newState.matches || [], newState.errors) : state.loaderData; // On a successful navigation we can assume we got through all blockers
1723
+ // so we can start fresh
1724
+
1725
+ for (let [key] of blockerFunctions) {
1726
+ deleteBlocker(key);
1727
+ } // Always respect the user flag. Otherwise don't reset on mutation
1728
+ // submission navigations unless they redirect
1729
+
1730
+
1731
+ let preventScrollReset = pendingPreventScrollReset === true || state.navigation.formMethod != null && isMutationMethod(state.navigation.formMethod) && ((_location$state2 = location.state) == null ? void 0 : _location$state2._isRedirect) !== true;
1620
1732
  updateState(_extends({}, newState, {
1621
1733
  // matches, errors, fetchers go through as-is
1622
1734
  actionData,
@@ -1626,9 +1738,9 @@ function createRouter(init) {
1626
1738
  initialized: true,
1627
1739
  navigation: IDLE_NAVIGATION,
1628
1740
  revalidation: "idle",
1629
- // Don't restore on submission navigations
1630
- restoreScrollPosition: state.navigation.formData ? false : getSavedScrollPosition(location, newState.matches || state.matches),
1631
- preventScrollReset: pendingPreventScrollReset
1741
+ restoreScrollPosition: getSavedScrollPosition(location, newState.matches || state.matches),
1742
+ preventScrollReset,
1743
+ blockers: new Map(state.blockers)
1632
1744
  }));
1633
1745
 
1634
1746
  if (isUninterruptedRevalidation) ; else if (pendingAction === exports.Action.Pop) ; else if (pendingAction === exports.Action.Push) {
@@ -1659,13 +1771,14 @@ function createRouter(init) {
1659
1771
  submission,
1660
1772
  error
1661
1773
  } = normalizeNavigateOptions(to, opts);
1662
- let location = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1774
+ let currentLocation = state.location;
1775
+ let nextLocation = createLocation(state.location, path, opts && opts.state); // When using navigate as a PUSH/REPLACE we aren't reading an already-encoded
1663
1776
  // URL from window.location, so we need to encode it here so the behavior
1664
1777
  // remains the same as POP and non-data-router usages. new URL() does all
1665
1778
  // the same encoding we'd get from a history.pushState/window.location read
1666
1779
  // without having to touch history
1667
1780
 
1668
- location = _extends({}, location, init.history.encodeLocation(location));
1781
+ nextLocation = _extends({}, nextLocation, init.history.encodeLocation(nextLocation));
1669
1782
  let userReplace = opts && opts.replace != null ? opts.replace : undefined;
1670
1783
  let historyAction = exports.Action.Push;
1671
1784
 
@@ -1680,7 +1793,41 @@ function createRouter(init) {
1680
1793
  }
1681
1794
 
1682
1795
  let preventScrollReset = opts && "preventScrollReset" in opts ? opts.preventScrollReset === true : undefined;
1683
- return await startNavigation(historyAction, location, {
1796
+ let blockerKey = shouldBlockNavigation({
1797
+ currentLocation,
1798
+ nextLocation,
1799
+ historyAction
1800
+ });
1801
+
1802
+ if (blockerKey) {
1803
+ // Put the blocker into a blocked state
1804
+ updateBlocker(blockerKey, {
1805
+ state: "blocked",
1806
+ location: nextLocation,
1807
+
1808
+ proceed() {
1809
+ updateBlocker(blockerKey, {
1810
+ state: "proceeding",
1811
+ proceed: undefined,
1812
+ reset: undefined,
1813
+ location: nextLocation
1814
+ }); // Send the same navigation through
1815
+
1816
+ navigate(to, opts);
1817
+ },
1818
+
1819
+ reset() {
1820
+ deleteBlocker(blockerKey);
1821
+ updateState({
1822
+ blockers: new Map(state.blockers)
1823
+ });
1824
+ }
1825
+
1826
+ });
1827
+ return;
1828
+ }
1829
+
1830
+ return await startNavigation(historyAction, nextLocation, {
1684
1831
  submission,
1685
1832
  // Send through the formData serialization error if we have one so we can
1686
1833
  // render at the right error boundary after we match routes
@@ -2452,7 +2599,9 @@ function createRouter(init) {
2452
2599
  await startNavigation(redirectHistoryAction, redirectLocation, {
2453
2600
  submission: _extends({}, submission, {
2454
2601
  formAction: redirect.location
2455
- })
2602
+ }),
2603
+ // Preserve this flag across redirects
2604
+ preventScrollReset: pendingPreventScrollReset
2456
2605
  });
2457
2606
  } else {
2458
2607
  // Otherwise, we kick off a new loading navigation, preserving the
@@ -2465,7 +2614,9 @@ function createRouter(init) {
2465
2614
  formAction: submission ? submission.formAction : undefined,
2466
2615
  formEncType: submission ? submission.formEncType : undefined,
2467
2616
  formData: submission ? submission.formData : undefined
2468
- }
2617
+ },
2618
+ // Preserve this flag across redirects
2619
+ preventScrollReset: pendingPreventScrollReset
2469
2620
  });
2470
2621
  }
2471
2622
  }
@@ -2584,6 +2735,78 @@ function createRouter(init) {
2584
2735
  return yeetedKeys.length > 0;
2585
2736
  }
2586
2737
 
2738
+ function getBlocker(key, fn) {
2739
+ let blocker = state.blockers.get(key) || IDLE_BLOCKER;
2740
+
2741
+ if (blockerFunctions.get(key) !== fn) {
2742
+ blockerFunctions.set(key, fn);
2743
+
2744
+ if (activeBlocker == null) {
2745
+ // This is now the active blocker
2746
+ activeBlocker = key;
2747
+ } else if (key !== activeBlocker) {
2748
+ warning(false, "A router only supports one blocker at a time");
2749
+ }
2750
+ }
2751
+
2752
+ return blocker;
2753
+ }
2754
+
2755
+ function deleteBlocker(key) {
2756
+ state.blockers.delete(key);
2757
+ blockerFunctions.delete(key);
2758
+
2759
+ if (activeBlocker === key) {
2760
+ activeBlocker = null;
2761
+ }
2762
+ } // Utility function to update blockers, ensuring valid state transitions
2763
+
2764
+
2765
+ function updateBlocker(key, newBlocker) {
2766
+ let blocker = state.blockers.get(key) || IDLE_BLOCKER; // Poor mans state machine :)
2767
+ // https://mermaid.live/edit#pako:eNqVkc9OwzAMxl8l8nnjAYrEtDIOHEBIgwvKJTReGy3_lDpIqO27k6awMG0XcrLlnz87nwdonESogKXXBuE79rq75XZO3-yHds0RJVuv70YrPlUrCEe2HfrORS3rubqZfuhtpg5C9wk5tZ4VKcRUq88q9Z8RS0-48cE1iHJkL0ugbHuFLus9L6spZy8nX9MP2CNdomVaposqu3fGayT8T8-jJQwhepo_UtpgBQaDEUom04dZhAN1aJBDlUKJBxE1ceB2Smj0Mln-IBW5AFU2dwUiktt_2Qaq2dBfaKdEup85UV7Yd-dKjlnkabl2Pvr0DTkTreM
2768
+
2769
+ invariant(blocker.state === "unblocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "blocked" || blocker.state === "blocked" && newBlocker.state === "proceeding" || blocker.state === "blocked" && newBlocker.state === "unblocked" || blocker.state === "proceeding" && newBlocker.state === "unblocked", "Invalid blocker state transition: " + blocker.state + " -> " + newBlocker.state);
2770
+ state.blockers.set(key, newBlocker);
2771
+ updateState({
2772
+ blockers: new Map(state.blockers)
2773
+ });
2774
+ }
2775
+
2776
+ function shouldBlockNavigation(_ref10) {
2777
+ let {
2778
+ currentLocation,
2779
+ nextLocation,
2780
+ historyAction
2781
+ } = _ref10;
2782
+
2783
+ if (activeBlocker == null) {
2784
+ return;
2785
+ } // We only allow a single blocker at the moment. This will need to be
2786
+ // updated if we enhance to support multiple blockers in the future
2787
+
2788
+
2789
+ let blockerFunction = blockerFunctions.get(activeBlocker);
2790
+ invariant(blockerFunction, "Could not find a function for the active blocker");
2791
+ let blocker = state.blockers.get(activeBlocker);
2792
+
2793
+ if (blocker && blocker.state === "proceeding") {
2794
+ // If the blocker is currently proceeding, we don't need to re-check
2795
+ // it and can let this navigation continue
2796
+ return;
2797
+ } // At this point, we know we're unblocked/blocked so we need to check the
2798
+ // user-provided blocker function
2799
+
2800
+
2801
+ if (blockerFunction({
2802
+ currentLocation,
2803
+ nextLocation,
2804
+ historyAction
2805
+ })) {
2806
+ return activeBlocker;
2807
+ }
2808
+ }
2809
+
2587
2810
  function cancelActiveDeferreds(predicate) {
2588
2811
  let cancelledRouteIds = [];
2589
2812
  activeDeferreds.forEach((dfd, routeId) => {
@@ -2676,6 +2899,8 @@ function createRouter(init) {
2676
2899
  getFetcher,
2677
2900
  deleteFetcher,
2678
2901
  dispose,
2902
+ getBlocker,
2903
+ deleteBlocker,
2679
2904
  _internalFetchControllers: fetchControllers,
2680
2905
  _internalActiveDeferreds: activeDeferreds
2681
2906
  };
@@ -3196,8 +3421,8 @@ function getMatchesToLoad(history, state, matches, submission, location, isReval
3196
3421
  cancelledDeferredRoutes.some(id => id === match.route.id) || shouldRevalidateLoader(history, state.location, state.matches[index], submission, location, match, isRevalidationRequired, actionResult))); // Pick fetcher.loads that need to be revalidated
3197
3422
 
3198
3423
  let revalidatingFetchers = [];
3199
- fetchLoadMatches && fetchLoadMatches.forEach((_ref10, key) => {
3200
- let [href, match, fetchMatches] = _ref10;
3424
+ fetchLoadMatches && fetchLoadMatches.forEach((_ref11, key) => {
3425
+ let [href, match, fetchMatches] = _ref11;
3201
3426
 
3202
3427
  // This fetcher was cancelled from a prior action submission - force reload
3203
3428
  if (cancelledFetcherLoads.includes(key)) {
@@ -3799,6 +4024,7 @@ function getTargetMatch(matches, location) {
3799
4024
 
3800
4025
  exports.AbortedDeferredError = AbortedDeferredError;
3801
4026
  exports.ErrorResponse = ErrorResponse;
4027
+ exports.IDLE_BLOCKER = IDLE_BLOCKER;
3802
4028
  exports.IDLE_FETCHER = IDLE_FETCHER;
3803
4029
  exports.IDLE_NAVIGATION = IDLE_NAVIGATION;
3804
4030
  exports.UNSAFE_DEFERRED_SYMBOL = UNSAFE_DEFERRED_SYMBOL;