navigation-stack 0.5.3 → 0.6.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.
Files changed (210) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +144 -282
  3. package/karma.conf.cjs +1 -1
  4. package/lib/cjs/NavigationStack.js +138 -49
  5. package/lib/cjs/data-storage/DataStorage.js +7 -6
  6. package/lib/cjs/environment/InMemoryEnvironment.js +6 -0
  7. package/lib/cjs/{session/ServerSideRenderSession.js → environment/ServerSideRenderEnvironment.js} +5 -6
  8. package/lib/cjs/environment/WebBrowserEnvironment.js +6 -0
  9. package/lib/cjs/environment/log/InMemoryLog.js +23 -0
  10. package/lib/cjs/environment/log/WebBrowserLog.js +22 -0
  11. package/lib/cjs/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  12. package/lib/cjs/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  13. package/lib/cjs/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  14. package/lib/cjs/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +2 -2
  15. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  16. package/lib/cjs/getLocationBaseFromLocation.js +14 -0
  17. package/lib/cjs/getLocationUrl.js +3 -5
  18. package/lib/cjs/index.js +10 -16
  19. package/lib/cjs/navigationBlockers.js +34 -32
  20. package/lib/cjs/navigationBlockersEvaluation.js +150 -0
  21. package/lib/cjs/parseInputLocation.js +2 -2
  22. package/lib/cjs/parseQueryFromSearch.js +3 -6
  23. package/lib/cjs/parseQueryString.js +77 -0
  24. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +7 -6
  25. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +31 -27
  26. package/lib/cjs/scroll-position/ScrollPositionSaver.js +6 -4
  27. package/lib/cjs/session/Session.js +61 -26
  28. package/lib/cjs/session/subscription/Subscription.js +36 -18
  29. package/lib/cjs/stringifyQuery.js +66 -0
  30. package/lib/cjs/stringifyQueryAsSearch.js +14 -0
  31. package/lib/esm/NavigationStack.js +138 -49
  32. package/lib/esm/data-storage/DataStorage.js +7 -6
  33. package/lib/esm/environment/InMemoryEnvironment.js +6 -0
  34. package/lib/esm/environment/ServerSideRenderEnvironment.js +10 -0
  35. package/lib/esm/environment/WebBrowserEnvironment.js +6 -0
  36. package/lib/esm/environment/log/InMemoryLog.js +17 -0
  37. package/lib/esm/environment/log/WebBrowserLog.js +16 -0
  38. package/lib/esm/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  39. package/lib/esm/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  40. package/lib/esm/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  41. package/lib/esm/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  42. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  43. package/lib/esm/getLocationBaseFromLocation.js +9 -0
  44. package/lib/esm/getLocationUrl.js +2 -5
  45. package/lib/esm/index.js +5 -8
  46. package/lib/esm/navigationBlockers.js +34 -32
  47. package/lib/esm/navigationBlockersEvaluation.js +145 -0
  48. package/lib/esm/parseInputLocation.js +2 -2
  49. package/lib/esm/parseQueryFromSearch.js +2 -6
  50. package/lib/esm/parseQueryString.js +72 -0
  51. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +7 -6
  52. package/lib/esm/scroll-position/ScrollPositionRestoration.js +31 -27
  53. package/lib/esm/scroll-position/ScrollPositionSaver.js +6 -4
  54. package/lib/esm/session/Session.js +61 -26
  55. package/lib/esm/session/subscription/Subscription.js +36 -18
  56. package/lib/esm/stringifyQuery.js +61 -0
  57. package/lib/esm/stringifyQueryAsSearch.js +8 -0
  58. package/lib/index.d.ts +180 -34
  59. package/package.json +4 -7
  60. package/src/NavigationStack.js +166 -56
  61. package/src/data-storage/DataStorage.js +9 -6
  62. package/src/environment/InMemoryEnvironment.js +6 -0
  63. package/src/environment/ServerSideRenderEnvironment.js +10 -0
  64. package/src/environment/WebBrowserEnvironment.js +6 -0
  65. package/src/environment/log/InMemoryLog.js +20 -0
  66. package/src/environment/log/WebBrowserLog.js +18 -0
  67. package/src/{session → environment}/navigation/InMemoryNavigation.js +16 -5
  68. package/src/{session → environment}/navigation/ServerSideNavigation.js +16 -7
  69. package/src/{session → environment}/navigation/WebBrowserNavigation.js +48 -8
  70. package/src/{session/navigation/error/ServerSideNavigationError.js → environment/navigation/error/ServerSideRedirectError.js} +1 -1
  71. package/src/environment/scroll-position/WebBrowserScrollPosition.js +15 -0
  72. package/src/getLocationBaseFromLocation.js +7 -0
  73. package/src/getLocationUrl.js +2 -5
  74. package/src/index.js +10 -13
  75. package/src/navigationBlockers.js +55 -34
  76. package/src/navigationBlockersEvaluation.js +161 -0
  77. package/src/parseInputLocation.js +2 -2
  78. package/src/parseQueryFromSearch.js +2 -6
  79. package/src/parseQueryString.js +81 -0
  80. package/src/scroll-position/ScrollPositionAutoSaver.js +10 -6
  81. package/src/scroll-position/ScrollPositionRestoration.js +36 -30
  82. package/src/scroll-position/ScrollPositionSaver.js +6 -4
  83. package/src/scroll-position/index.js +1 -1
  84. package/src/session/Session.js +68 -24
  85. package/src/session/subscription/Subscription.js +36 -11
  86. package/src/stringifyQuery.js +71 -0
  87. package/src/stringifyQueryAsSearch.js +9 -0
  88. package/test/NavigationStack.addBasePath.test.js +50 -0
  89. package/test/{redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockNonProgrammaticNavigationIfRequired.test.js} +51 -63
  90. package/test/{redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js → NavigationStack.blockProgrammaticNavigationIfRequired.test.js} +98 -78
  91. package/test/NavigationStack.general.test.js +68 -0
  92. package/test/NavigationStack.parseInputLocation.test.js +52 -0
  93. package/test/NavigationStack.removeBasePath.test.js +69 -0
  94. package/test/NavigationStack.test.js +97 -29
  95. package/test/data-storage/LocationDataStorage.test.js +3 -2
  96. package/test/index.js +7 -31
  97. package/test/index.test.js +4 -5
  98. package/test/parseQueryFromSearch.test.js +19 -0
  99. package/test/parseQueryString.test.js +18 -0
  100. package/test/scroll-position/ScrollPositionRestoration.test.js +34 -13
  101. package/test/scroll-position/createApp.js +8 -8
  102. package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +4 -4
  103. package/test/session/{InMemorySession.test.js → Session.InMemoryEnvironment.test.js} +10 -9
  104. package/test/session/{ServerSession.test.js → Session.ServerSideRenderEnvironment.test.js} +5 -4
  105. package/test/session/{WebBrowserSession.test.js → Session.WebBrowserEnvironment.test.js} +63 -13
  106. package/test/shouldWarn.js +44 -0
  107. package/test/stringifyQuery.test.js +65 -0
  108. package/types/index.d.ts +180 -34
  109. package/types/tsconfig.json +0 -1
  110. package/data-storage/package.json +0 -7
  111. package/lib/cjs/createSearchFromQuery.js +0 -13
  112. package/lib/cjs/debug.js +0 -12
  113. package/lib/cjs/redux/ActionTypes.js +0 -14
  114. package/lib/cjs/redux/ActionTypesInternal.js +0 -8
  115. package/lib/cjs/redux/Actions.js +0 -28
  116. package/lib/cjs/redux/createMiddlewares.js +0 -60
  117. package/lib/cjs/redux/index.js +0 -13
  118. package/lib/cjs/redux/internalLocationReducer.js +0 -14
  119. package/lib/cjs/redux/locationReducer.js +0 -13
  120. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -32
  121. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -113
  122. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  123. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -30
  124. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -73
  125. package/lib/cjs/redux/middleware/navigationOperationMiddleware.js +0 -40
  126. package/lib/cjs/redux/middleware/parseInputLocationMiddleware.js +0 -29
  127. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +0 -34
  128. package/lib/cjs/session/InMemorySession.js +0 -22
  129. package/lib/cjs/session/WebBrowserSession.js +0 -20
  130. package/lib/data-storage/index.d.ts +0 -35
  131. package/lib/esm/createSearchFromQuery.js +0 -8
  132. package/lib/esm/debug.js +0 -7
  133. package/lib/esm/redux/ActionTypes.js +0 -9
  134. package/lib/esm/redux/ActionTypesInternal.js +0 -3
  135. package/lib/esm/redux/Actions.js +0 -22
  136. package/lib/esm/redux/createMiddlewares.js +0 -54
  137. package/lib/esm/redux/index.js +0 -4
  138. package/lib/esm/redux/internalLocationReducer.js +0 -8
  139. package/lib/esm/redux/locationReducer.js +0 -7
  140. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  141. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -108
  142. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -88
  143. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -25
  144. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -68
  145. package/lib/esm/redux/middleware/navigationOperationMiddleware.js +0 -35
  146. package/lib/esm/redux/middleware/parseInputLocationMiddleware.js +0 -24
  147. package/lib/esm/redux/middleware/updateLocationMiddleware.js +0 -28
  148. package/lib/esm/session/InMemorySession.js +0 -15
  149. package/lib/esm/session/ServerSideRenderSession.js +0 -11
  150. package/lib/esm/session/WebBrowserSession.js +0 -13
  151. package/lib/redux/index.d.ts +0 -90
  152. package/lib/scroll-position/index.d.ts +0 -107
  153. package/redux/package.json +0 -7
  154. package/scroll-position/package.json +0 -7
  155. package/src/createSearchFromQuery.js +0 -9
  156. package/src/debug.js +0 -8
  157. package/src/redux/ActionTypes.js +0 -9
  158. package/src/redux/ActionTypesInternal.js +0 -3
  159. package/src/redux/Actions.js +0 -27
  160. package/src/redux/createMiddlewares.js +0 -65
  161. package/src/redux/index.js +0 -4
  162. package/src/redux/internalLocationReducer.js +0 -9
  163. package/src/redux/locationReducer.js +0 -8
  164. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +0 -27
  165. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +0 -119
  166. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +0 -94
  167. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +0 -26
  168. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +0 -72
  169. package/src/redux/middleware/navigationOperationMiddleware.js +0 -34
  170. package/src/redux/middleware/parseInputLocationMiddleware.js +0 -23
  171. package/src/redux/middleware/updateLocationMiddleware.js +0 -28
  172. package/src/session/InMemorySession.js +0 -13
  173. package/src/session/ServerSideRenderSession.js +0 -9
  174. package/src/session/WebBrowserSession.js +0 -13
  175. package/test/middlewareTestUtil.js +0 -31
  176. package/test/redux/Action.test.js +0 -73
  177. package/test/redux/ActionTypes.test.js +0 -13
  178. package/test/redux/createMiddlewares.test.js +0 -96
  179. package/test/redux/index.test.js +0 -10
  180. package/test/redux/locationReducer.test.js +0 -39
  181. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +0 -40
  182. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +0 -51
  183. package/test/redux/middleware/navigationOperationMiddleware.test.js +0 -78
  184. package/test/redux/middleware/parseInputLocationMiddleware.test.js +0 -62
  185. package/test/testUtil.js +0 -3
  186. package/types/data-storage/index.d.ts +0 -35
  187. package/types/redux/index.d.ts +0 -90
  188. package/types/scroll-position/index.d.ts +0 -107
  189. /package/lib/cjs/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  190. /package/lib/cjs/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  191. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  192. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  193. /package/lib/cjs/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  194. /package/lib/cjs/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  195. /package/lib/cjs/{session → environment}/navigation/operation/operations.js +0 -0
  196. /package/lib/esm/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  197. /package/lib/esm/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  198. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  199. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  200. /package/lib/esm/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  201. /package/lib/esm/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  202. /package/lib/esm/{session → environment}/navigation/operation/operations.js +0 -0
  203. /package/src/{session → environment}/lifecycle/InMemorySessionLifecycle.js +0 -0
  204. /package/src/{session → environment}/lifecycle/WebBrowserSessionLifecycle.js +0 -0
  205. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycle.js +0 -0
  206. /package/src/{session → environment}/lifecycle/page-lifecycle/PageLifecycleInstance.js +0 -0
  207. /package/src/{session → environment}/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +0 -0
  208. /package/src/{session → environment}/navigation/error/NavigationOutOfBoundsError.js +0 -0
  209. /package/src/{session → environment}/navigation/operation/operations.js +0 -0
  210. /package/test/{parseInputLocationMiddleware.test.js → parseInputLocation.test.js} +0 -0
@@ -1,81 +1,170 @@
1
- const _excluded = ["maintainScrollPosition"];
2
- function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
3
- import { applyMiddleware, createStore } from 'redux';
4
- import Actions from './redux/Actions';
5
- import createMiddlewares from './redux/createMiddlewares';
6
- import locationReducer from './redux/locationReducer';
1
+ import { addBasePath, removeBasePath } from './basePath';
2
+ import LocationDataStorage from './data-storage/LocationDataStorage';
3
+ import getLocationFromInternalLocation from './getLocationFromInternalLocation';
4
+ import isPromise from './isPromise';
5
+ import { addNavigationBlocker, removeAllNavigationBlockers } from './navigationBlockers';
6
+ import { blockNonProgrammaticNavigationIfRequired, blockProgrammaticNavigationIfRequired } from './navigationBlockersEvaluation';
7
+ import parseInputLocation from './parseInputLocation';
7
8
  import ScrollPositionRestoration from './scroll-position/ScrollPositionRestoration';
8
- function getCreateMiddlewaresOptions(navigationStackOptions) {
9
- if (!navigationStackOptions) {
10
- return undefined;
11
- }
12
- // eslint-disable-next-line no-unused-vars
13
- const restOptions = _objectWithoutPropertiesLoose(navigationStackOptions, _excluded);
14
- return restOptions;
15
- }
9
+ import Session from './session/Session';
16
10
  export default class NavigationStack {
17
- constructor(session, options) {
18
- this._session = session;
11
+ constructor(Environment, {
12
+ basePath,
13
+ manageScrollPosition,
14
+ scrollPositionSetter
15
+ } = {}) {
16
+ // Allows temporarily ignoring location update events.
17
+ this._doAndIgnoreLocationUpdates = func => {
18
+ this._ignoreLocationUpdates = true;
19
+ func();
20
+ this._ignoreLocationUpdates = false;
21
+ };
22
+ // Create a session.
23
+ this._session = new Session(Environment);
24
+
25
+ // Base path, if used.
26
+ this._basePath = basePath;
27
+
28
+ // Create location data storage.
29
+ this.dataStorage = new LocationDataStorage(this._session, {
30
+ namespace: 'navigation-stack'
31
+ });
19
32
 
20
- // Create a Redux store.
21
- this._store = createStore(locationReducer, applyMiddleware(...createMiddlewares(session, getCreateMiddlewaresOptions(options))));
33
+ // Allows temporarily ignoring location update events when set to `true`.
34
+ this._ignoreLocationUpdates = false;
35
+
36
+ // Subscribe to location updates.
37
+ // * Ignores location updates if `_ignoreLocationUpdates` flag is temporarily set.
38
+ // * Runs navigation blockers to see if the location update should be reverted.
39
+ // * Updates `this._location` if the update wasn't ignored or blocked.
40
+ this._unsubscribe = this._session.subscribe(location => {
41
+ // If this location update shouldn't be temporarily ignored.
42
+ if (!this._ignoreLocationUpdates) {
43
+ // Remove `basePath` from `location`.
44
+ location = removeBasePath(location, this._basePath);
45
+
46
+ // See if the location update should've been blocked.
47
+ // If it should've, it will automatically "rewind" it.
48
+ const result = blockNonProgrammaticNavigationIfRequired(location, this._session, this._doAndIgnoreLocationUpdates);
49
+ const onResult = blocked => {
50
+ if (!blocked) {
51
+ // Update `this._location`.
52
+ // Since it's gonna be returned from the public `this.current()` method,
53
+ // convert it from `LocationInternal` to `Location`.
54
+ this._location = getLocationFromInternalLocation(location);
55
+ }
56
+ };
57
+ if (isPromise(result)) {
58
+ result.then(onResult);
59
+ } else {
60
+ onResult(result);
61
+ }
62
+ }
63
+ });
22
64
 
23
65
  // Create `ScrollPositionRestoration`.
24
- if (options && options.maintainScrollPosition) {
25
- this._scrollPositionRestoration = new ScrollPositionRestoration(session);
66
+ if (manageScrollPosition) {
67
+ this._scrollPositionRestoration = new ScrollPositionRestoration(this._session,
68
+ // Custom `ScrollPositionSetter`.
69
+ {
70
+ scrollPositionSetter
71
+ });
26
72
  }
27
73
  }
74
+
75
+ // Subscribes to any changes of the current location.
76
+ // The first subscriber is always the `NavigationStack` itself
77
+ // because its listener is what drives the actual navigation.
78
+ // Any additional application-specific listeners could be added, if required.
79
+ subscribe(listener) {
80
+ // `NavigationStack.subscribe()` is simply a proxy to `Session.subscribe()`
81
+ // with the only convenience feature that it "normalizes" the `location` argument.
82
+ return this._session.subscribe(locationInternal => {
83
+ listener(getLocationFromInternalLocation(locationInternal));
84
+ });
85
+ }
86
+ addNavigationBlocker(blocker) {
87
+ return addNavigationBlocker(this._session, blocker);
88
+ }
28
89
  addScrollableContainer(scrollableContainerKey, scrollableContainer) {
29
90
  if (!this._scrollPositionRestoration) {
30
- throw new Error('`maintainScrollPosition: true` option not passed');
91
+ throw new Error('`manageScrollPosition: true` option not passed');
31
92
  }
32
93
  return this._scrollPositionRestoration.addScrollableContainer(scrollableContainerKey, scrollableContainer);
33
94
  }
34
- subscribe(listener) {
35
- // Subscribe to any potential Redux state changes.
36
- return this._store.subscribe(() => {
37
- // Initially, calls the listener when setting the initial location.
38
- // After that, calls it on any location change.
39
- const location = this.current();
40
- if (!this._latestLocation || location !== this._latestLocation) {
41
- this._latestLocation = location;
42
- listener(location);
43
- }
44
- });
45
- }
46
95
  init(initialLocation) {
47
- if (this._latestLocation) {
96
+ if (this._location) {
48
97
  throw new Error('Already initialized');
49
98
  }
50
- this._store.dispatch(Actions.init(initialLocation));
51
- this._latestLocation = this.current();
99
+ this._session.start(initialLocation && this._parseInputLocation(initialLocation));
100
+ if (this._scrollPositionRestoration) {
101
+ this._scrollPositionRestoration.start();
102
+ }
52
103
  }
53
104
  current() {
54
- return this._store.getState();
105
+ // TypeScript definition of the `.current()` method tells that it always returns
106
+ // some non-`undefined` location.
107
+ // But `this._location` is `undefined` until `.init(initialLocation?)` is called.
108
+ // To work around that limitation, it simply throws if `.current()` is called before `.init()`.
109
+ if (!this._location) {
110
+ throw new Error('Not initialized');
111
+ }
112
+ return this._location;
55
113
  }
56
114
  push(location) {
57
- this._store.dispatch(Actions.push(location));
115
+ this._navigate('push', location);
58
116
  }
59
117
  replace(location) {
60
- this._store.dispatch(Actions.replace(location));
118
+ this._navigate('replace', location);
119
+ }
120
+ _navigate(operation, location) {
121
+ const toLocation = this._parseInputLocation(location);
122
+ const result = blockProgrammaticNavigationIfRequired(toLocation, this._session);
123
+ const onResult = blocked => {
124
+ if (!blocked) {
125
+ this._session.navigate(operation, toLocation);
126
+ }
127
+ };
128
+ if (isPromise(result)) {
129
+ result.then(onResult);
130
+ } else {
131
+ onResult(result);
132
+ }
61
133
  }
62
134
  shift(delta) {
63
- this._store.dispatch(Actions.shift(delta));
135
+ this._session.shift(delta);
64
136
  }
65
137
  stop() {
138
+ if (!this._unsubscribe) {
139
+ throw new Error('Already stopped');
140
+ }
141
+ this._unsubscribe();
142
+ this._unsubscribe = undefined;
143
+
144
+ // Even if it calls `unsubscribe()` function above, any other subscriptions
145
+ // would still stay. We're not talking about `navigationStack.subscribe()`
146
+ // subscriptions because those don't really matter in terms of cleaning them up:
147
+ // those're just Redux store subscriptions that don't have any side effects.
148
+ // Subscriptions we're talking here are `Session`'s own subscription
149
+ // via `session.subscribe()` and any hypothetical manual `session.subscribe()`
150
+ // calls that could be made by the application code for whatever purpose.
151
+ // Both of those should be cleared.
152
+ // To work around that, `.stop()` function removes all subscriptions.
153
+ this._session.stop();
154
+ removeAllNavigationBlockers(this._session);
66
155
  if (this._scrollPositionRestoration) {
67
156
  this._scrollPositionRestoration.stop();
68
157
  }
69
- this._store.dispatch(Actions.stop());
70
158
  }
71
- locationRendered() {
72
- if (this._scrollPositionRestoration) {
73
- const location = this.current();
74
- if (!location) {
75
- throw new Error('Not initialized');
76
- }
77
- return this._scrollPositionRestoration.locationRendered(location);
159
+ locationRendered(location) {
160
+ if (!this._scrollPositionRestoration) {
161
+ throw new Error('`manageScrollPosition: true` option not passed');
78
162
  }
79
- return Promise.resolve();
163
+ return this._scrollPositionRestoration.locationRendered(location);
164
+ }
165
+ _parseInputLocation(inputLocation) {
166
+ // Parse input location (string or incomplete object) to a proper `location` object.
167
+ // Add `basePath` to `location`.
168
+ return addBasePath(parseInputLocation(inputLocation), this._basePath);
80
169
  }
81
170
  }
@@ -6,6 +6,7 @@ export default class DataStorage {
6
6
  throw new Error('`DataStorage` requires a `session.key`');
7
7
  }
8
8
  this._sessionKey = session.key;
9
+ this._log = session.environment.log;
9
10
  this._dataStorage = session.environment.dataStorage;
10
11
  this._namespace = namespace;
11
12
  }
@@ -22,8 +23,8 @@ export default class DataStorage {
22
23
  // junk into sessionStorage under our namespace.
23
24
  return JSON.parse(value);
24
25
  } catch (error) {
25
- // eslint-disable-next-line no-console
26
- console.error('[navigation-stack] Could not read data from storage');
26
+ this._log.error('[navigation-stack] Could not read data from storage');
27
+ this._log.error(error);
27
28
 
28
29
  // Pretend that the entry doesn't exist.
29
30
  return undefined;
@@ -36,8 +37,8 @@ export default class DataStorage {
36
37
  this._dataStorage.remove(storageKey);
37
38
  } catch (error) {
38
39
  // No need to handle errors here.
39
- // eslint-disable-next-line no-console
40
- console.error('[navigation-stack] Could not delete data from storage');
40
+ this._log.error('[navigation-stack] Could not delete data from storage');
41
+ this._log.error(error);
41
42
  }
42
43
  return;
43
44
  }
@@ -50,8 +51,8 @@ export default class DataStorage {
50
51
  } catch (error) {
51
52
  // No need to handle errors here either. If it didn't work, it didn't
52
53
  // work. We make no guarantees about actually saving the value.
53
- // eslint-disable-next-line no-console
54
- console.error('[navigation-stack] Could not save data in storage');
54
+ this._log.error('[navigation-stack] Could not save data in storage');
55
+ this._log.error(error);
55
56
  }
56
57
  }
57
58
 
@@ -1,8 +1,14 @@
1
1
  import InMemoryDataStorage from './data-storage/InMemoryDataStorage';
2
+ import InMemorySessionLifecycle from './lifecycle/InMemorySessionLifecycle';
3
+ import InMemoryLog from './log/InMemoryLog';
4
+ import InMemoryNavigation from './navigation/InMemoryNavigation';
2
5
  import InMemoryScrollPosition from './scroll-position/InMemoryScrollPosition';
3
6
  export default class InMemoryEnvironment {
4
7
  constructor() {
5
8
  this.dataStorage = new InMemoryDataStorage();
9
+ this.log = new InMemoryLog();
10
+ this.lifecycle = new InMemorySessionLifecycle();
11
+ this.navigation = new InMemoryNavigation();
6
12
  this.scrollPosition = new InMemoryScrollPosition();
7
13
  }
8
14
  }
@@ -0,0 +1,10 @@
1
+ import InMemoryEnvironment from './InMemoryEnvironment';
2
+ import ServerSideNavigation from './navigation/ServerSideNavigation';
3
+
4
+ // `ServerSideRenderSession` is just a `InMemorySession` that specifically prohibits any navigation.
5
+ export default class ServerSideRenderEnvironment extends InMemoryEnvironment {
6
+ constructor() {
7
+ super();
8
+ this.navigation = new ServerSideNavigation();
9
+ }
10
+ }
@@ -1,8 +1,14 @@
1
1
  import WebBrowserDataStorage from './data-storage/WebBrowserDataStorage';
2
+ import WebBrowserSessionLifecycle from './lifecycle/WebBrowserSessionLifecycle';
3
+ import WebBrowserLog from './log/WebBrowserLog';
4
+ import WebBrowserNavigation from './navigation/WebBrowserNavigation';
2
5
  import WebBrowserScrollPosition from './scroll-position/WebBrowserScrollPosition';
3
6
  export default class WebBrowserEnvironment {
4
7
  constructor() {
5
8
  this.dataStorage = new WebBrowserDataStorage();
6
9
  this.scrollPosition = new WebBrowserScrollPosition();
10
+ this.lifecycle = new WebBrowserSessionLifecycle();
11
+ this.log = new WebBrowserLog();
12
+ this.navigation = new WebBrowserNavigation();
7
13
  }
8
14
  }
@@ -0,0 +1,17 @@
1
+ const DEBUG_ENABLED = false;
2
+ export default class InMemoryLog {
3
+ debug(...args) {
4
+ if (DEBUG_ENABLED) {
5
+ // eslint-disable-next-line no-console
6
+ console.log(...args);
7
+ }
8
+ }
9
+ warn(...args) {
10
+ // eslint-disable-next-line no-console
11
+ console.warn(...args);
12
+ }
13
+ error(...args) {
14
+ // eslint-disable-next-line no-console
15
+ console.error(...args);
16
+ }
17
+ }
@@ -0,0 +1,16 @@
1
+ export default class WebBrowserLog {
2
+ debug(...args) {
3
+ if (window.NAVIGATION_STACK_DEBUG_ENABLED) {
4
+ // eslint-disable-next-line no-console
5
+ console.log(...args);
6
+ }
7
+ }
8
+ warn(...args) {
9
+ // eslint-disable-next-line no-console
10
+ console.warn(...args);
11
+ }
12
+ error(...args) {
13
+ // eslint-disable-next-line no-console
14
+ console.error(...args);
15
+ }
16
+ }
@@ -5,12 +5,23 @@ export default class InMemoryNavigation {
5
5
  this._stack = [];
6
6
  }
7
7
 
8
+ // Subscribes to any "asynchronous" changes of the current location,
9
+ // "asynchronous" changes being ones that happen out-of-sync with the code
10
+ // that might have potentially triggered those changes.
11
+ //
12
+ // For example, in a web browser, "Back"/"Forward" navigation happens out-of-sync
13
+ // with the code that calls `window.pushState()` or `window.replaceState()` function.
14
+ //
15
+ // Additionally, in a web browser, "Back"/"Forward" navigation could be triggered
16
+ // outside of the application code by user clicking those "Back"/"Forward" buttons manually
17
+ // in their web browser.
18
+ //
8
19
  // eslint-disable-next-line no-unused-vars
9
- subscribe(listener) {
10
- // `InMemoryNavigation` doesn't have any "asynchronycity" about it
11
- // and performs any navigation immediately at the time of the call.
12
- // Hence, no asynchronous listener would ever be called
13
- // due to no asynchronous events being dispatched.
20
+ subscribeToAsyncrhonousLocationUpdates(listener) {
21
+ // `InMemoryNavigation` location changes are always "synchronous"
22
+ // with the code that initiated such changes, i.e. it always performs
23
+ // any navigation immediately at the time such navigation is triggered in code.
24
+ // Hence, this function doesn't have to "subscribe" to anything, so it's a "no op".
14
25
  return () => {};
15
26
  }
16
27
  init(initialLocation, {
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable no-underscore-dangle, max-classes-per-file */
2
2
 
3
- import ServerSideNavigationError from './error/ServerSideNavigationError';
3
+ import ServerSideRedirectError from './error/ServerSideRedirectError';
4
4
  export default class ServerSideNavigation {
5
5
  init(initialLocation, {
6
6
  operation,
@@ -16,12 +16,21 @@ export default class ServerSideNavigation {
16
16
  });
17
17
  }
18
18
 
19
+ // Subscribes to any "asynchronous" changes of the current location,
20
+ // "asynchronous" changes being ones that happen out-of-sync with the code
21
+ // that might have potentially triggered those changes.
22
+ //
23
+ // For example, in a web browser, "Back"/"Forward" navigation happens out-of-sync
24
+ // with the code that calls `window.pushState()` or `window.replaceState()` function.
25
+ //
26
+ // Additionally, in a web browser, "Back"/"Forward" navigation could be triggered
27
+ // outside of the application code by user clicking those "Back"/"Forward" buttons manually
28
+ // in their web browser.
29
+ //
19
30
  // eslint-disable-next-line no-unused-vars
20
- subscribe(listener) {
21
- // `ServerSideNavigation` doesn't have any "asynchronycity" about it
22
- // and any navigation is prohibited and would result in an error.
23
- // So no asynchronous listener would ever be called
24
- // due to no asynchronous events being dispatched.
31
+ subscribeToAsyncrhonousLocationUpdates(listener) {
32
+ // `ServerSideNavigation` location changes are prohibited, so they couldn't happen.
33
+ // Hence, this function doesn't have to "subscribe" to anything, so it's a "no op".
25
34
  return () => {};
26
35
  }
27
36
 
@@ -32,7 +41,7 @@ export default class ServerSideNavigation {
32
41
  index,
33
42
  delta
34
43
  }) {
35
- throw new ServerSideNavigationError(location);
44
+ throw new ServerSideRedirectError(location);
36
45
  }
37
46
 
38
47
  // eslint-disable-next-line no-unused-vars
@@ -47,15 +47,28 @@ export default class WebBrowserNavigation {
47
47
  this._currentLocationIndex = NO_LOCATION_INDEX;
48
48
  }
49
49
 
50
- // Subscribes to changes in the current location.
50
+ // Subscribes to any "asynchronous" changes of the current location,
51
+ // "asynchronous" changes being ones that happen out-of-sync with the code
52
+ // that might have potentially triggered those changes.
53
+ //
54
+ // For example, in a web browser, "Back"/"Forward" navigation happens out-of-sync
55
+ // with the code that calls `window.pushState()` or `window.replaceState()` function.
56
+ //
57
+ // Additionally, in a web browser, "Back"/"Forward" navigation could be triggered
58
+ // outside of the application code by user clicking those "Back"/"Forward" buttons manually
59
+ // in their web browser.
60
+ //
51
61
  // Returns an `unsubscribe()` function which is "idempotent", i.e. it can be called multiple times.
52
- subscribe(listener) {
62
+ //
63
+ subscribeToAsyncrhonousLocationUpdates(listener) {
53
64
  const onPopState = () => {
54
65
  // If "popstate" event is received before navigation is initialized,
55
- // ignore such "popstate" event. This behavior is logical from the application code's view.
66
+ // ignore such "popstate" event. Such "ignore" behavior is logical from
67
+ // the application code's point of view: it doesn't expect any navigation events
68
+ // to be recorded before it has initialized the navigation.
56
69
  // And besides, `this._currentLocationIndex` is not defined in such conditions.
57
70
  if (this._currentLocationIndex === NO_LOCATION_INDEX) {
58
- throw new Error('Received a "popstate" event before initialized');
71
+ throw new Error('Received a "popstate" event before finished initializing navigation');
59
72
  }
60
73
  const prevIndex = this._currentLocationIndex;
61
74
  const {
@@ -86,6 +99,16 @@ export default class WebBrowserNavigation {
86
99
  this._subscribed = false;
87
100
  };
88
101
  }
102
+
103
+ // When run in a web browser, it could not only "start" a new navigation session
104
+ // but also "resume" a previously-started navigation session. That could happen
105
+ // when the user refreshes a page in a web browser which still retains
106
+ // the previous navigation session's data but at the same time restarts
107
+ // the javascript code from scratch.
108
+ //
109
+ // So this `init()` method handles both cases: when there's previous navigation session's data
110
+ // that should be restored and when there's no previous navigation session's data.
111
+ //
89
112
  init(initialLocation, {
90
113
  operation,
91
114
  key,
@@ -102,7 +125,8 @@ export default class WebBrowserNavigation {
102
125
  // by calling `window.history.replaceState()` on page load.
103
126
  // Otherwise, `window.history.state` would be `null` for the initial location
104
127
  // and there'd be no place to store the additional properties of the initial location
105
- // such as `location.key`.
128
+ // such as `location.key`. Without `location.key` always being present the initial location object,
129
+ // there'd be a bug of incorrect scroll position being set on the initial location URL in some scenarios:
106
130
  // https://github.com/taion/scroll-behavior/issues/215
107
131
  //
108
132
  // If the user opens the initial page for the first time, `window.history.state` will be `null`.
@@ -116,7 +140,7 @@ export default class WebBrowserNavigation {
116
140
  index
117
141
  };
118
142
  // Call `history.replaceState()`.
119
- this._storeAdditionalPropertiesForLocation(initialLocation, additionalProperties, delta);
143
+ this._navigateToLocationAndKeepItsAdditionalPropertiesInHistory(initialLocation, additionalProperties, delta);
120
144
  }
121
145
  this._currentLocationIndex = index;
122
146
 
@@ -136,7 +160,7 @@ export default class WebBrowserNavigation {
136
160
  key,
137
161
  index
138
162
  };
139
- this._storeAdditionalPropertiesForLocation(location, additionalProperties, delta);
163
+ this._navigateToLocationAndKeepItsAdditionalPropertiesInHistory(location, additionalProperties, delta);
140
164
  this._currentLocationIndex = index;
141
165
 
142
166
  // Call the listeners.
@@ -198,7 +222,23 @@ export default class WebBrowserNavigation {
198
222
  delta
199
223
  };
200
224
  }
201
- _storeAdditionalPropertiesForLocation(location, additionalProperties, delta) {
225
+
226
+ // Stores "additional" properties associated with `location` in web browser's history storage.
227
+ // Web browser's history storage is not intended for large datasets and should only be used
228
+ // to store small bits of data.
229
+ //
230
+ // "Some browsers save state objects to the user's disk so they can be restored after the user restarts
231
+ // the browser, and impose a size limit on the serialized representation of a state object, and will throw
232
+ // an exception if you pass a state object whose serialized representation is larger than that size limit.
233
+ // So in cases where you want to ensure you have more space than what some browsers might impose,
234
+ // you're encouraged to use sessionStorage and/or localStorage."
235
+ //
236
+ // Source: https://developer.mozilla.org/en-US/docs/Web/API/History/pushState
237
+ //
238
+ // To store large amounts of data, one could use `window.sessionStorage` instead.
239
+ // It is accessible via `DataStorage(session)` class.
240
+ //
241
+ _navigateToLocationAndKeepItsAdditionalPropertiesInHistory(location, additionalProperties, delta) {
202
242
  const url = getLocationUrl(location);
203
243
  // `delta` property is not stored in `window.history.state`
204
244
  // because it is supposed to be recalculated every time when reading from `window.history.state`.
@@ -1,7 +1,7 @@
1
1
  const _excluded = ["operation"];
2
2
  function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
3
3
  import getLocationUrl from '../../../getLocationUrl';
4
- export default class ServerSideNavigationError extends Error {
4
+ export default class ServerSideRedirectError extends Error {
5
5
  constructor(location) {
6
6
  super(location ? `Navigate to ${getLocationUrl(location)}` : 'Navigate to previous or next location');
7
7
  if (location) {
@@ -45,9 +45,24 @@ export default class WebBrowserScrollPosition {
45
45
  };
46
46
  }
47
47
  enableAutomaticScrollRestoration() {
48
+ // The default "auto" behavior seems to work in the following way:
49
+ //
50
+ // * It doesn't scroll to top on `window.history.pushState()` or `window.history.replaceState()`.
51
+ //
52
+ // * It does restore scroll position on "popstate" event
53
+ // (they say, in Firefox it happens before the event is dispatched,
54
+ // while in Chrome it happens after the event is dispatched)
55
+ //
56
+ // https://v5.reactrouter.com/web/guides/scroll-restoration
57
+ //
48
58
  window.history.scrollRestoration = 'auto';
49
59
  }
50
60
  disableAutomaticScrollRestoration() {
61
+ // Setting `window.history.scrollRestoration` value updates it in the current history entry
62
+ // and any subsequent history entries.
63
+ // This means that it should be set at application initialization stage,
64
+ // that is before any navigation.
65
+ // https://majido.github.io/scroll-restoration-proposal/history-based-api.html
51
66
  window.history.scrollRestoration = 'manual';
52
67
  }
53
68
  init() {}
@@ -0,0 +1,9 @@
1
+ const _excluded = ["key", "index"];
2
+ function _objectWithoutPropertiesLoose(r, e) { if (null == r) return {}; var t = {}; for (var n in r) if ({}.hasOwnProperty.call(r, n)) { if (-1 !== e.indexOf(n)) continue; t[n] = r[n]; } return t; }
3
+ // Converts `Location` object to a `LocationBase` object.
4
+ // It hides properties of location such as `key` or `index`.
5
+ export default function getLocationBaseFromLocation(location) {
6
+ // eslint-disable-next-line no-unused-vars
7
+ const locationBase = _objectWithoutPropertiesLoose(location, _excluded);
8
+ return locationBase;
9
+ }
@@ -1,4 +1,4 @@
1
- import { stringify as stringifyQuery } from 'query-string';
1
+ import stringifyQueryAsSearch from './stringifyQueryAsSearch';
2
2
  export default function getLocationUrl({
3
3
  pathname,
4
4
  search,
@@ -6,10 +6,7 @@ export default function getLocationUrl({
6
6
  hash
7
7
  }) {
8
8
  if (!search && query) {
9
- const queryString = stringifyQuery(query);
10
- if (queryString) {
11
- search = `?${queryString}`;
12
- }
9
+ search = stringifyQueryAsSearch(query);
13
10
  }
14
11
  return `${pathname}${search || ''}${hash || ''}`;
15
12
  }
package/lib/esm/index.js CHANGED
@@ -4,11 +4,8 @@ export { default as getLocationUrl } from './getLocationUrl';
4
4
  export { default as parseLocationUrl } from './parseLocationUrl';
5
5
  export { default as parseInputLocation } from './parseInputLocation';
6
6
  export { default as NavigationStack } from './NavigationStack';
7
- export { default as DataStorage } from './data-storage/DataStorage';
8
- export { default as LocationDataStorage } from './data-storage/LocationDataStorage';
9
- export { default as Session } from './session/Session';
10
- export { default as InMemorySession } from './session/InMemorySession';
11
- export { default as WebBrowserSession } from './session/WebBrowserSession';
12
- export { default as ServerSideRenderSession } from './session/ServerSideRenderSession';
13
- export { default as ServerSideNavigationError } from './session/navigation/error/ServerSideNavigationError';
14
- export { default as NavigationOutOfBoundsError } from './session/navigation/error/NavigationOutOfBoundsError';
7
+ export { default as InMemoryEnvironment } from './environment/InMemoryEnvironment';
8
+ export { default as WebBrowserEnvironment } from './environment/WebBrowserEnvironment';
9
+ export { default as ServerSideRenderEnvironment } from './environment/ServerSideRenderEnvironment';
10
+ export { default as ServerSideRedirectError } from './environment/navigation/error/ServerSideRedirectError';
11
+ export { default as NavigationOutOfBoundsError } from './environment/navigation/error/NavigationOutOfBoundsError';