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