navigation-stack 0.3.0 → 0.4.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 (253) hide show
  1. package/README.md +603 -163
  2. package/data-storage/package.json +6 -0
  3. package/karma.conf.cjs +21 -4
  4. package/lib/cjs/NavigationStack.js +73 -0
  5. package/lib/cjs/data-storage/DataStorage.js +71 -0
  6. package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
  7. package/lib/cjs/data-storage/index.js +9 -0
  8. package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
  9. package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
  10. package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
  11. package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
  12. package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
  13. package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
  14. package/lib/cjs/getLocationFromInternalLocation.js +14 -0
  15. package/lib/cjs/index.js +20 -16
  16. package/lib/cjs/navigationBlockers.js +25 -23
  17. package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
  18. package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  19. package/lib/cjs/redux/ActionTypesInternal.js +8 -0
  20. package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
  21. package/lib/cjs/redux/createMiddlewares.js +60 -0
  22. package/lib/cjs/redux/index.js +13 -0
  23. package/lib/cjs/redux/internalLocationReducer.js +14 -0
  24. package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
  25. package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
  26. package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  27. package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
  28. package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
  29. package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
  30. package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
  31. package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
  32. package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
  33. package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +130 -0
  34. package/lib/cjs/scroll-position/ScrollPositionRestoration.js +383 -0
  35. package/lib/cjs/scroll-position/ScrollPositionSaver.js +81 -0
  36. package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
  37. package/lib/cjs/scroll-position/constants.js +5 -0
  38. package/lib/cjs/scroll-position/index.js +7 -0
  39. package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
  40. package/lib/cjs/session/InMemorySession.js +22 -0
  41. package/lib/cjs/session/ServerSideRenderSession.js +17 -0
  42. package/lib/cjs/session/Session.js +196 -0
  43. package/lib/cjs/session/WebBrowserSession.js +20 -0
  44. package/lib/cjs/session/key/createSessionKey.js +23 -0
  45. package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
  46. package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
  47. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
  48. package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
  49. package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
  50. package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
  51. package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
  52. package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
  53. package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
  54. package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
  55. package/lib/cjs/session/navigation/operation/operations.js +11 -0
  56. package/lib/cjs/session/subscription/Subscription.js +81 -0
  57. package/lib/data-storage/index.d.ts +35 -0
  58. package/lib/esm/NavigationStack.js +66 -0
  59. package/lib/esm/data-storage/DataStorage.js +65 -0
  60. package/lib/esm/data-storage/LocationDataStorage.js +22 -0
  61. package/lib/esm/data-storage/index.js +2 -0
  62. package/lib/esm/environment/InMemoryEnvironment.js +8 -0
  63. package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
  64. package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
  65. package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
  66. package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
  67. package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
  68. package/lib/esm/getLocationFromInternalLocation.js +9 -0
  69. package/lib/esm/index.js +10 -8
  70. package/lib/esm/navigationBlockers.js +25 -23
  71. package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
  72. package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  73. package/lib/esm/redux/ActionTypesInternal.js +3 -0
  74. package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
  75. package/lib/esm/redux/createMiddlewares.js +54 -0
  76. package/lib/esm/redux/index.js +4 -0
  77. package/lib/esm/redux/internalLocationReducer.js +8 -0
  78. package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  79. package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
  80. package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
  81. package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
  82. package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
  83. package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
  84. package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  85. package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
  86. package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
  87. package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +123 -0
  88. package/lib/esm/scroll-position/ScrollPositionRestoration.js +376 -0
  89. package/lib/esm/scroll-position/ScrollPositionSaver.js +74 -0
  90. package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
  91. package/lib/esm/scroll-position/constants.js +1 -0
  92. package/lib/esm/scroll-position/index.js +1 -0
  93. package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
  94. package/lib/esm/session/InMemorySession.js +15 -0
  95. package/lib/esm/session/ServerSideRenderSession.js +11 -0
  96. package/lib/esm/session/Session.js +189 -0
  97. package/lib/esm/session/WebBrowserSession.js +13 -0
  98. package/lib/esm/session/key/createSessionKey.js +18 -0
  99. package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  100. package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
  101. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
  102. package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
  103. package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
  104. package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
  105. package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
  106. package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
  107. package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
  108. package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
  109. package/lib/esm/session/navigation/operation/operations.js +6 -0
  110. package/lib/esm/session/subscription/Subscription.js +75 -0
  111. package/lib/index.d.ts +178 -157
  112. package/lib/redux/index.d.ts +90 -0
  113. package/lib/scroll-position/index.d.ts +107 -0
  114. package/package.json +9 -5
  115. package/redux/package.json +6 -0
  116. package/scroll-position/package.json +6 -0
  117. package/src/NavigationStack.js +84 -0
  118. package/src/data-storage/DataStorage.js +69 -0
  119. package/src/data-storage/LocationDataStorage.js +23 -0
  120. package/src/data-storage/index.js +2 -0
  121. package/src/environment/InMemoryEnvironment.js +9 -0
  122. package/src/environment/WebBrowserEnvironment.js +9 -0
  123. package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
  124. package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
  125. package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
  126. package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
  127. package/src/getLocationFromInternalLocation.js +7 -0
  128. package/src/index.js +10 -8
  129. package/src/navigationBlockers.js +28 -27
  130. package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
  131. package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
  132. package/src/redux/ActionTypesInternal.js +3 -0
  133. package/src/{Actions.js → redux/Actions.js} +4 -3
  134. package/src/redux/createMiddlewares.js +65 -0
  135. package/src/redux/index.js +4 -0
  136. package/src/redux/internalLocationReducer.js +9 -0
  137. package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
  138. package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
  139. package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
  140. package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
  141. package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
  142. package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
  143. package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
  144. package/src/redux/middleware/updateLocationMiddleware.js +28 -0
  145. package/src/scroll-position/PageScrollPositionSetter.js +110 -0
  146. package/src/scroll-position/ScrollPositionAutoSaver.js +151 -0
  147. package/src/scroll-position/ScrollPositionRestoration.js +506 -0
  148. package/src/scroll-position/ScrollPositionSaver.js +100 -0
  149. package/src/scroll-position/ScrollPositionSetter.js +16 -0
  150. package/src/scroll-position/constants.js +1 -0
  151. package/src/scroll-position/index.js +1 -0
  152. package/src/scroll-position/scheduleNextTick.js +6 -0
  153. package/src/session/InMemorySession.js +13 -0
  154. package/src/session/ServerSideRenderSession.js +9 -0
  155. package/src/session/Session.js +216 -0
  156. package/src/session/WebBrowserSession.js +13 -0
  157. package/src/session/key/createSessionKey.js +18 -0
  158. package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
  159. package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
  160. package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
  161. package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
  162. package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
  163. package/src/session/navigation/InMemoryNavigation.js +78 -0
  164. package/src/session/navigation/ServerSideNavigation.js +43 -0
  165. package/src/session/navigation/WebBrowserNavigation.js +224 -0
  166. package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
  167. package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
  168. package/src/session/navigation/operation/operations.js +6 -0
  169. package/src/session/subscription/Subscription.js +76 -0
  170. package/test/NavigationStack.test.js +296 -0
  171. package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
  172. package/test/data-storage/index.test.js +8 -0
  173. package/test/index.js +12 -0
  174. package/test/index.test.js +8 -7
  175. package/test/{helpers.js → middlewareTestUtil.js} +9 -12
  176. package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
  177. package/test/{Action.test.js → redux/Action.test.js} +7 -6
  178. package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
  179. package/test/redux/createMiddlewares.test.js +96 -0
  180. package/test/redux/index.test.js +10 -0
  181. package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
  182. package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
  183. package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
  184. package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
  185. package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
  186. package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
  187. package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
  188. package/test/scroll-position/ScrollPositionRestoration.test.js +418 -0
  189. package/test/scroll-position/addScrollableContainer.js +36 -0
  190. package/test/scroll-position/addScrollableContainerWithHyperlink.js +50 -0
  191. package/test/scroll-position/createApp.js +112 -0
  192. package/test/scroll-position/delay.js +9 -0
  193. package/test/scroll-position/mockPageLifecycle.js +17 -0
  194. package/test/scroll-position/runApp.js +24 -0
  195. package/test/scroll-position/withScrollableContainerAtIndexPage.js +62 -0
  196. package/test/session/InMemorySession.test.js +348 -0
  197. package/test/session/ServerSession.test.js +17 -9
  198. package/test/session/WebBrowserSession.test.js +265 -0
  199. package/test/testUtil.js +3 -0
  200. package/types/data-storage/index.d.ts +35 -0
  201. package/types/index.d.ts +178 -157
  202. package/types/redux/index.d.ts +90 -0
  203. package/types/scroll-position/index.d.ts +107 -0
  204. package/types/tsconfig.json +1 -1
  205. package/lib/cjs/LocationDataStorage.js +0 -60
  206. package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
  207. package/lib/cjs/beforeLocationChangeListeners.js +0 -51
  208. package/lib/cjs/createMiddlewares.js +0 -47
  209. package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
  210. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
  211. package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
  212. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
  213. package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
  214. package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
  215. package/lib/cjs/session/BrowserSession.js +0 -235
  216. package/lib/cjs/session/MemorySession.js +0 -223
  217. package/lib/cjs/session/ServerSession.js +0 -65
  218. package/lib/esm/LocationDataStorage.js +0 -53
  219. package/lib/esm/addBeforeLocationChangeListener.js +0 -2
  220. package/lib/esm/beforeLocationChangeListeners.js +0 -44
  221. package/lib/esm/createMiddlewares.js +0 -41
  222. package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
  223. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
  224. package/lib/esm/middleware/createLocationMiddleware.js +0 -50
  225. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
  226. package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
  227. package/lib/esm/onlyAllowedOnClientSide.js +0 -5
  228. package/lib/esm/session/BrowserSession.js +0 -229
  229. package/lib/esm/session/MemorySession.js +0 -217
  230. package/lib/esm/session/ServerSession.js +0 -58
  231. package/src/LocationDataStorage.js +0 -59
  232. package/src/addBeforeLocationChangeListener.js +0 -2
  233. package/src/beforeLocationChangeListeners.js +0 -54
  234. package/src/createMiddlewares.js +0 -45
  235. package/src/middleware/createBasePathMiddleware.js +0 -20
  236. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
  237. package/src/middleware/createLocationMiddleware.js +0 -55
  238. package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
  239. package/src/middleware/createTransformLocationMiddleware.js +0 -29
  240. package/src/onlyAllowedOnClientSide.js +0 -5
  241. package/src/session/BrowserSession.js +0 -235
  242. package/src/session/MemorySession.js +0 -219
  243. package/src/session/ServerSession.js +0 -67
  244. package/test/createMiddlewares.test.js +0 -62
  245. package/test/middleware/createBasePathMiddleware.test.js +0 -67
  246. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
  247. package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
  248. package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
  249. package/test/session/BrowserSession.test.js +0 -182
  250. package/test/session/MemorySession.test.js +0 -244
  251. /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
  252. /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
  253. /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
package/README.md CHANGED
@@ -3,9 +3,11 @@
3
3
  [![npm version](https://img.shields.io/npm/v/navigation-stack.svg?style=flat-square)](https://www.npmjs.com/package/navigation-stack)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/navigation-stack.svg?style=flat-square)](https://www.npmjs.com/package/navigation-stack)
5
5
 
6
- Handles navigation in a web browser. Represents web browser navigation history as a "stack" data structure. Provides operations to perform programmatic navigation such as "push" (go to new URL), "replace" (redirect to new URL), "shift" (rewind to a previously visited URL). Provides a subscription mechanism to get notified on current location change.
6
+ Handles navigation in a web browser. Represents web browser navigation history as a "stack" data structure. Provides operations to perform programmatic navigation such as "push" (go to new URL), "replace" (redirect to new URL), "shift" (rewind to a previously visited URL). Provides a subscription mechanism to get notified on location changes.
7
7
 
8
- Originally forked from [`farce`](http://npmjs.com/package/farce) package to fix a [bug](https://github.com/4Catalyzer/farce/issues/483).
8
+ Also supports automatic [scroll position restoration](#scroll-position-restoration) on "Back"/"Forward" navigation.
9
+
10
+ Originally forked from [`farce`](http://npmjs.com/package/farce) package to fix a couple of small bugs ([1](https://github.com/4Catalyzer/farce/issues/483), [2](https://github.com/4Catalyzer/farce/issues/491)). Then merged it with [`scroll-behavior`](http://npmjs.com/package/scroll-behavior) package to fix a couple of small bugs ([1](https://github.com/taion/scroll-behavior/issues/215), [2](https://github.com/taion/scroll-behavior/pull/472)). Then decided to completely rewrite the entire code and changed the API to my liking.
9
11
 
10
12
  ## Install
11
13
 
@@ -15,60 +17,97 @@ npm install navigation-stack
15
17
 
16
18
  ## Use
17
19
 
18
- `navigation-stack` provides "middlewares", "actions" and a "reducer" that could be used with `redux` or any other `redux`-compatible package such as [`mini-redux`](https://www.npmjs.com/package/mini-redux).
19
-
20
- ```js
21
- import { createStore, applyMiddleware } from 'redux'
20
+ Any changes to a `NavigationStack` instance are "magically" reflected in the web browser's address bar and navigation history, and vice versa: any changes to the URL in the web browser's address bar are "magically" reflected in the `NavigationStack` instance. So one could think of `NavigationStack` as a very convenient proxy to web browser's address bar and navigation history. What's left to the application is to subscribe to `navigationStack` changes and re-render the page accordingly.
22
21
 
23
- import {
24
- createMiddlewares,
25
- locationReducer,
26
- Actions,
27
- BrowserSession
28
- } from 'navigation-stack'
22
+ Start by creating a `NavigationStack` instance.
29
23
 
30
- // Create a Redux store.
31
- const store = createStore(
32
- locationReducer, // Reducer function. For example, `locationReducer()`.
33
- applyMiddleware(...createMiddlewares(new BrowserSession()))
34
- )
24
+ ```js
25
+ import { NavigationStack, WebBrowserSession } from 'navigation-stack'
35
26
 
36
- // Initialize navigation.
37
- store.dispatch(Actions.init())
27
+ // Create a `NavigationStack` instance.
28
+ // It should be tied to a navigation "session".
29
+ const navigationStack = new NavigationStack(new WebBrowserSession())
38
30
  ```
39
31
 
40
- After that, dispatch any of the `Actions` in order to navigate.
32
+ Then subscribe to changes:
41
33
 
42
34
  ```js
43
- // To navigate to a new page.
44
- store.dispatch(Actions.push('/new/location'))
45
-
46
- // To redirect to a new page.
47
- store.dispatch(Actions.replace('/new/location'))
35
+ // Subscribe to location changes.
36
+ // The first call happens for the initial location.
37
+ // Next calls will happen in case of navigation.
38
+ const unsubscribe = navigationStack.subscribe((location) => {
39
+ console.log('Current location', location)
40
+ document.body.innerHTML = '<div>' + location.pathname + '</div>'
41
+ })
42
+ ```
48
43
 
49
- // To go back.
50
- store.dispatch(Actions.shift(-1))
44
+ Now ready to perform navigation actions.
51
45
 
52
- // To go forward.
53
- store.dispatch(Actions.shift(1))
46
+ ```js
47
+ // Sets the initial location.
48
+ navigationStack.init()
49
+
50
+ // Sets the `location` to be a new location.
51
+ //
52
+ // Also updates the URL in the web browser's address bar.
53
+ //
54
+ // Also adds a new entry in the web browser's navigation history.
55
+ //
56
+ navigationStack.push('/new-location')
57
+
58
+ // Sets the `location` to be a new location.
59
+ //
60
+ // Also updates the URL in the web browser's address bar.
61
+ //
62
+ // Does not add a new entry in the web browser's navigation history
63
+ // which is the only difference between this and `Actions.push()`.
64
+ //
65
+ navigationStack.replace('/new-location')
66
+
67
+ // Sets the `location` to be a previous one (if there is one).
68
+ // If there's no such `location` in the navigation history, throws a `NavigationOutOfBoundsError`.
69
+ //
70
+ // One could think of it as an equivalent of clicking a "Back" button in a web browser.
71
+ //
72
+ // Also updates the URL in the web browser's address bar.
73
+ //
74
+ // Also shifts the current position in the web browser's navigation history.
75
+ //
76
+ navigationStack.shift(-1)
77
+
78
+ // Sets the `location` to be a next one (if there is one).
79
+ // If there's no such `location` in the navigation history, throws a `NavigationOutOfBoundsError`.
80
+ //
81
+ // One could think of it as an equivalent of clicking a "Forward" button in a web browser.
82
+ //
83
+ // Also updates the URL in the web browser's address bar.
84
+ //
85
+ // Also shifts the current position in the web browser's navigation history.
86
+ //
87
+ navigationStack.shift(1)
54
88
  ```
55
89
 
56
- To view the current location:
90
+ To get the current location:
57
91
 
58
92
  ```js
59
- // When `locationReducer()` is used,
60
- // `store.getState()` is the current location.
61
- console.log(store.getState())
93
+ const location = navigationStack.current()
94
+ console.log(location)
62
95
  ```
63
96
 
64
- (optional) (advanced) Stop and clean up:
97
+ (optional) After the user is done using the app, stop the session and clean up any listeners.
65
98
 
66
99
  ```js
67
- store.dispatch(Actions.dispose())
100
+ // (optional)
101
+ // When the user closes the application,
102
+ // stop the session and clean up any listeners.
103
+ // There's no need to do this in a web browser.
104
+ unsubscribe()
105
+ navigationStack.stop()
68
106
  ```
69
107
 
70
108
  ## Current Location
71
109
 
110
+ <!--
72
111
  To track the current location, the application could listen to `ActionTypes.UPDATE` action. The `payload` of the action is the current location.
73
112
 
74
113
  For example, below is the source code for the default `locationReducer`.
@@ -88,27 +127,18 @@ function reducer(state, action) {
88
127
 
89
128
  With this reducer, `store.getState()` will return the current location.
90
129
 
91
- Calling `store.dispatch(Actions.init())` will trigger the initial `ActionTypes.UPDATE` action which will set the initial current location. From then on, the current location will always stay in sync with the web browser's URL bar, including "Back"/"Forward" navigation.
130
+ Calling `store.dispatch(Actions.init(window.location))` will trigger the initial `ActionTypes.UPDATE` action which will set the initial current location. From then on, the current location will always stay in sync with the web browser's URL bar, including "Back"/"Forward" navigation.
131
+ -->
92
132
 
93
- A `location` object has all the properties of a [standard web browser location](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) with the addition of:
133
+ To get the current location, use `navigationStack.current()`.
134
+
135
+ Current `location` object has all the properties of a [standard web browser location](https://developer.mozilla.org/en-US/docs/Web/API/Window/location) with the addition of:
94
136
  * `query: object` — URL query parameters.
95
- * `index: number` — The index of the location in the navigation history, starting with `0` for the initial location.
96
- * `action: string` — The type of navigation that led to the location.
97
- * `INIT` in case of the initial location before any navigation has taken place.
98
- * `SHIFT` when the user performs a "Back" or "Forward" navigation, or after a `.shift()` navigation which is essentially a "back or forward navigation".
99
- * `PUSH` in case of a `.push()` navigation, i.e. "normal navigation via a hyperlink".
100
- * `REPLACE` in case of a `.replace()` navigation, i.e. "redirect".
101
- * `delta: number` — the difference between the `index` of the current location and the `index` of the previous location.
102
- * `0` for the initial location before any navigation has taken place.
103
- * `1` after a `.push()` navigation, i.e. "normal navigation via a hyperlink".
104
- * `0` after a `.replace()` navigation, i.e. "redirect".
105
- * `delta: number` after a `.shift(delta)` navigation, i.e. "back or forward navigation".
106
- * `-1` after the user clicks a "Back" button in their web browser.
107
- * `1` after the user clicks a "Forward" button in their web browser.
108
- * `key: string` — a unique ID of the `location` object within the navigation history.
109
-
110
- ## Subscribe to Location Changes
137
+ * `key: string` — a unique ID of the `location` object within the session.
138
+
139
+ <!-- ## Subscribe to Location Changes -->
111
140
 
141
+ <!--
112
142
  One could use Redux'es standard [subscription mechanisms](https://redux.js.org/api/store#subscribelistener) to immediately get notified of current location changes.
113
143
 
114
144
  ```js
@@ -117,7 +147,7 @@ let currentLocation
117
147
  // Create a Redux store.
118
148
  const store = createStore(
119
149
  locationReducer, // Reducer function. For example, `locationReducer()`.
120
- applyMiddleware(...createMiddlewares(new BrowserSession()))
150
+ applyMiddleware(...createMiddlewares(new WebBrowserSession()))
121
151
  )
122
152
 
123
153
  // Subscribe to any potential Redux state changes.
@@ -125,204 +155,608 @@ const unsubscribe = store.subscribe(() => {
125
155
  const previousLocation = currentLocation
126
156
  currentLocation = store.getState() // In case of using `locationReducer()`.
127
157
  if (currentLocation !== previousLocation) {
158
+ // The first time is for the initial location.
159
+ // Next times will happen in case of navigation.
128
160
  console.log('Location has changed')
129
161
  }
130
162
  })
131
163
 
132
- // Initialize navigation.
133
- // Emitting a Redux action will trigger the listener.
134
- store.dispatch(Actions.init())
164
+ // Initialize navigation with an initial location.
165
+ //
166
+ // It will trigger the listener.
167
+ //
168
+ store.dispatch(Actions.init(window.location))
135
169
 
136
170
  // Stop listening to current location changes.
137
171
  unsubscribe()
138
172
  ```
173
+ -->
174
+
175
+ <!--
176
+ One could subscribe to location changes by calling `navigationStack.subscribe()`.
139
177
 
178
+ ```js
179
+ // Create a `NavigationStack` instance.
180
+ // It should be tied to a navigation "session".
181
+ const navigationStack = new NavigationStack(new WebBrowserSession())
182
+
183
+ // Subscribe to location changes.
184
+ // The first call happens for the initial location.
185
+ // Next calls will happen in case of navigation.
186
+ const unsubscribe = navigationStack.subscribe((location) => {
187
+ console.log('Current location', location)
188
+ })
189
+
190
+ // Navigate to a new location.
191
+ // It will trigger the listener.
192
+ navigationStack.push('/new-location')
193
+
194
+ // Stop listening to location changes.
195
+ unsubscribe()
196
+ ```
197
+ -->
198
+
199
+ <!--
140
200
  ## Why Redux?
141
201
 
142
202
  Why complicate things by providing "middlewares", "actions" and a "reducer" when it could be just a conventional API? That's because always knowing the "current location" means having to deal with "state management" in one way or another, and the simplest and most popular "state management" toolkit to date seems to be Redux.
143
203
 
144
204
  If it was just about dispatching the `Actions` then of course it wouldn't require any "state management". But it's the "get current location" piece that changes the whole picture. One could say that using Redux for such a simple task is an overkill but actually reinventing a wheel is what I would consider "overkill". It's like crafting your own screwdriver just because the one from Walmart feels too bulky.
205
+ -->
145
206
 
146
- ## Session
207
+ ## Scroll Position Restoration
147
208
 
148
- Navigation is performed within a given "session". A "session" sets the boundaries within a given navigation session exists, so that each user (and then each their browser tab) would have a separate navigation session. A specific "session" implementation maps `navigation-stack` concepts to their physical execution, such as web browser API. Three different "session" implementations are shipped with this package: `BrowserSession`, `ServerSession` and `MemorySession`.
209
+ Pass `maintainScrollPosition: true` option to keep track of scroll position on every page and then automatically restore it on "Back" or "Forward" navigation.
149
210
 
150
211
  ```js
151
- import {
152
- BrowserSession,
153
- ServerSession,
154
- MemorySession
155
- } from 'navigation-stack'
212
+ import { NavigationStack, WebBrowserSession } from 'navigation-stack'
213
+
214
+ // Create a `NavigationStack` instance with a `maintainScrollPosition: true` option.
215
+ const navigationStack = new NavigationStack(new WebBrowserSession(), {
216
+ maintainScrollPosition: true
217
+ })
218
+
219
+ //----------------------------------------------------------------------------------------
220
+
221
+ // Sets the initial location.
222
+ navigationStack.init()
223
+
224
+ // Render the initial location.
225
+ document.body.innerHTML = '<div> Initial Location </div>'
226
+
227
+ // When a page has been rendered, tell `NavigationStack` to restore
228
+ // a previously-saved scroll position, if there's any.
229
+ //
230
+ // This method must be called both for the initial location and any subsequent location.
231
+ //
232
+ navigationStack.locationRendered()
233
+
234
+ //----------------------------------------------------------------------------------------
235
+
236
+ // Set the `location` to be a new location.
237
+ //
238
+ // This also updates the URL in the web browser's address bar
239
+ // and adds a new entry in the web browser's navigation history.
240
+ //
241
+ navigationStack.push('/new-location')
242
+
243
+ // Render the new location.
244
+ document.body.innerHTML = '<div> New Location </div>'
156
245
 
157
- new BrowserSession()
158
- new ServerSession('/initial-location-url')
159
- new MemorySession('/initial-location-url')
246
+ // The new location is now rendered.
247
+ // There's no scroll position to restore because it's not a previously-visited location.
248
+ navigationStack.locationRendered()
249
+
250
+ //----------------------------------------------------------------------------------------
251
+
252
+ // Set `location` "back" to the initial location.
253
+ //
254
+ // This also updates the URL in the web browser's address bar
255
+ // and repositions the "current location" pointer in the web browser's navigation history.
256
+ //
257
+ navigationStack.shift(-1)
258
+
259
+ // Render the initial location.
260
+ document.body.innerHTML = '<div> Initial Location </div>'
261
+
262
+ // The initial location is now rendered.
263
+ // Restores the scroll position at the initial location.
264
+ navigationStack.locationRendered()
265
+
266
+ //----------------------------------------------------------------------------------------
267
+
268
+ // (optional)
269
+ // When the user is about to close the application,
270
+ // stop the `NavigationStack` and clean up any of its listeners.
271
+ // This is not required in a web browser because it cleans up all listeners
272
+ // automatically when closing a tab.
273
+ navigationStack.stop()
160
274
  ```
161
275
 
162
- - Use `BrowserSession` in a web browser. The navigation session is automatically limited to a given web browser tab and survives a page refresh.
163
- - Use `ServerSession` in server-side rendering. Create a separate `ServerSession` for each incoming HTTP request.
164
- - Use `MemorySession` in tests to mimick a `BrowserSession`. Create a separate `MemorySession` for each separate navigation session.
165
- - `MemorySession` supports saving and restoring its state, althrough there doesn't seem to be any real-world use for this feature. Yet, it exists. To enable state restoration, pass an optional second argument an `options` object with properties:
166
- - `save(key: string, data: string)` — Saves `data` under the `key`.
167
- - `load(key: string)` — Loads the data stored under the `key`.
276
+ `NavigationStack` provides methods:
277
+
278
+ * `addScrollableContainer(key: string, element: Element)` — Use it in cases when it should restore not only the page scroll position but also the scroll position(s) of any other scrollable container(s). Returns a "remove scrollable container" function.
279
+ * `locationRendered()` Call it every time a different location has been rendered, i.e. immediately after a different location has been rendered, including the initial location.
280
+
281
+ <details>
282
+ <summary>Using scroll position restoration feature without <code>NavigationStack</code></summary>
283
+
284
+ ######
285
+
286
+ To use the scroll position restoration feature independently of a `NavigationStack` instance (e.g. without it), create a `ScrollPositionRestoration` instance and pass a `session` argument to it.
287
+
288
+ ```js
289
+ import { WebBrowserSession } from 'navigation-stack'
290
+ import { ScrollPositionRestoration } from 'navigation-stack/scroll-position'
291
+
292
+ // Create a `ScrollPositionRestoration`.
293
+ const scrollPositionRestoration = new ScrollPositionRestoration(new WebBrowserSession())
294
+
295
+ //----------------------------------------------------------------------------------------
296
+
297
+ // If you decide to use `NavigationStack` or Redux-way `createMiddlewares()` for navigation,
298
+ // it should be tied to the same session.
299
+ //
300
+ // const navigationStack = new NavigationStack(session)
301
+ // navigationStack.init()
302
+ //
303
+ // Or, navigation could be performed by any other means such as using `window.history.pushState()`.
304
+ // The only requirement is for the "current location" object to have a `key` property
305
+ // which the standard `window.location` object doesn't provide.
306
+ //
307
+ window.history.replaceState({ key: '123' }, '', '/initial-location')
308
+
309
+ // Render the initial location.
310
+ document.body.innerHTML = '<div> Initial Location </div>'
311
+
312
+ // When a page has been rendered, call "did render location" listener
313
+ // with the "current location" object as the argument.
314
+ scrollPositionRestoration.locationRendered({ key: '123', pathname: '/initial-location' })
315
+
316
+ //----------------------------------------------------------------------------------------
317
+
318
+ // Navigate to a new location.
319
+ //
320
+ // For example, it could use `NavigationStack` for navigation.
321
+ //
322
+ // navigationStack.push('/new-location')
323
+ //
324
+ // Or, navigation could be performed by any other means such as using `window.history.pushState()`.
325
+ //
326
+ window.history.pushState({ key: '456' }, '', '/new-location')
327
+
328
+ // Render the new location.
329
+ document.body.innerHTML = '<div> New Location </div>'
330
+
331
+ // The new location is now rendered.
332
+ // There's no scroll position to restore because it's not a previously-visited location.
333
+ // The "current location" object must have a `key`.
334
+ scrollPositionRestoration.locationRendered({ key: '456', pathname: '/new-location' })
335
+
336
+ //----------------------------------------------------------------------------------------
337
+
338
+ // Navigate "back" to the initial location.
339
+ //
340
+ // For example, it could use `NavigationStack` for navigation.
341
+ //
342
+ // navigationStack.shift(-1)
343
+ //
344
+ // Or, navigation could be performed by any other means such as using `window.history.go()`.
345
+ //
346
+ window.history.go(-1)
347
+
348
+ // Render the initial location.
349
+ document.body.innerHTML = '<div> Initial Location </div>'
350
+
351
+ // The initial location is now rendered.
352
+ // Restores the scroll position at the initial location.
353
+ // The "current location" object must have a `key`.
354
+ scrollPositionRestoration.locationRendered({ key: '123', pathname: '/initial-location' })
355
+
356
+ //----------------------------------------------------------------------------------------
357
+
358
+ // (optional)
359
+ // When the user is about to close the application,
360
+ // stop the `ScrollPositionRestoration` and clean up any of its listeners.
361
+ // This is not required in a web browser because it cleans up all listeners
362
+ // automatically when closing a tab.
363
+ scrollPositionRestoration.stop()
364
+
365
+ // (optional)
366
+ // In case of using `NavigationStack` for navigation,
367
+ // stop the `NavigationStack` and clean up any of its listeners.
368
+ //
369
+ // navigationStack.stop()
370
+ ```
371
+
372
+ `ScrollPositionRestoration` provides methods:
373
+
374
+ * `addScrollableContainer(key: string, element: Element)` — Use it in cases when it should restore not only the page scroll position but also the scroll position(s) of any other scrollable container(s). Returns a "remove scrollable container" function.
375
+ * `locationRendered(location)` — Call it every time a different location has been rendered, i.e. immediately after a different location has been rendered, including the initial location. The location argument should be a `navigation-stack` location.
376
+ * `stop()` — Stops scroll position restoration and clears any listeners or timers.
377
+ </details>
168
378
 
169
379
  ## Base Path
170
380
 
381
+ <!--
171
382
  If the web application is hosted under a certain URL prefix, it should be specified in `createMiddlewares()` call as `basePath` parameter.
172
383
 
173
384
  ```js
174
- createMiddlewares(session, { basePath?: '/base/path' })
385
+ createMiddlewares(session, { basePath?: '/base-path' })
175
386
  ```
387
+ -->
176
388
 
177
- ## Location State Storage
178
-
179
- One could use `LocationDataStorage` in order to store location-specific data. For example, one could store scroll position of a page and then restore that scroll position when the user decides to navigate "Back" to the page.
389
+ If the web application is hosted under a certain URL prefix, it should be specified as a `basePath` parameter when creating a `NavigationStack` instance. This prefix will automatically be added to the URL in the web browser's address bar while the `location` object itself won't include it in the `pathname`.
180
390
 
181
391
  ```js
182
- import { BrowserSession, LocationDataStorage } from 'navigation-stack'
392
+ new NavigationStack(new WebBrowserSession(), { basePath: '/base-path' })
393
+ ```
183
394
 
184
- const session = new BrowserSession()
395
+ ## Session
185
396
 
186
- const storage = new LocationDataStorage(session, { namespace?: 'optional-namespace' })
397
+ A "session" ties `NavigationStack` to the environment it operates in, such as a web browser.
187
398
 
188
- const location = { pathname: '/abc' }
399
+ Three different "session" implementations are shipped with this package:
189
400
 
190
- storage.set(location, 'key', 123)
191
- storage.get(location, 'key') === 123
192
- ```
401
+ - Use `WebBrowserSession` in a web browser. Such session survives a page refresh and is automatically destroyed when the web browser tab gets closed.
402
+ - Use `ServerSideRenderSession` in server-side rendering. Create a separate session for each incoming HTTP request. Initialize it with a relative URL of the HTTP request. If, during server-side render, the application code attempts to navigate to another location, it will throw a `ServerSideNavigationError` with a `location` property in it.
403
+ - Use `InMemorySession` in tests to mimick a `WebBrowserSession`. Create a separate session for each separate navigation session. Initialize it with a relative URL or a location object.
404
+
405
+ <details>
406
+ <summary>See <code>ServerSideRenderSession</code> example</summary>
407
+
408
+ ######
409
+
410
+ ```js
411
+ const navigationStack = new NavigationStack(new ServerSideRenderSession())
412
+
413
+ navigationStack.subscribe((location) => {
414
+ console.log('Current location', location)
415
+ })
193
416
 
194
- `LocationDataStorage` doesn't provide any guarantees about actually storing the data: if it encounters any errors in the process, it simply ignores them. This simplifies the API in a way that the application doesn't have to wrap `.get()`/`.set()` calls in a `try/catch` block. And judging by the nature of location-specific data, that type of data is inherently non-essential and rather "nice-to-have".
417
+ // Sets the initial location.
418
+ // Triggers the subscription listener.
419
+ navigationStack.init('/initial-location')
420
+
421
+ // Navigates to a new location.
422
+ // Throws `ServerSideNavigationError` with a `location` property.
423
+ navigationStack.push('/new-location')
424
+ ```
425
+ </details>
195
426
 
196
- One might ask: Why use `LocationDataStorage` when one could simply store the data in a usual variable? The answer is that a usual variable doesn't survive if the user decides to refresh the page. But the entire navigation history does survive because that's how web browsers work. So if the user decides to go "Back" after refreshing the current page, the data associated to that previous location would already be lost and can't be recovered. In contrast, when using a `LocationDataStorage` with a `BrowserSession`, the stored data does survive a page refresh, which feels more consistent and coherent with the persistence behavior of the navigation history itself.
197
427
 
198
- ## Get Notified Before Location Changes
428
+ <details>
429
+ <summary>See <code>InMemorySession</code> example</summary>
199
430
 
200
- One could subscribe to "before change" events of the current location by calling `addBeforeLocationChangeListener()` exported function. The listener function will be called before the `location` object in Redux state is updated. This might be a suitable opportunity to save location-specific state such as the scroll position.
431
+ ######
201
432
 
202
433
  ```js
203
- import { createStore, applyMiddleware } from 'redux'
434
+ const navigationStack = new NavigationStack(new InMemorySession())
435
+
436
+ navigationStack.subscribe((location) => {
437
+ console.log('Current location', location)
438
+ })
439
+
440
+ // Sets the initial location.
441
+ // Triggers the subscription listener.
442
+ navigationStack.init('/initial-location')
443
+
444
+ // Navigates to a new location.
445
+ // Triggers the subscription listener.
446
+ navigationStack.push('/new-location')
447
+ ```
448
+ </details>
449
+
450
+ ######
451
+
452
+ Once created, a "session" is simply passed to the `NavigationStack` constructor and then you don't have to deal with it anymore — `NavigationStack` will pull all the strings for you.
453
+
454
+ However, if someone prefers to completely bypass `NavigationStack` and interact with a "session" object directly, they could do so.
455
+
456
+ <details>
457
+ <summary>See "session" API</summary>
458
+
459
+ ######
460
+
461
+ * `key: string` — A unique ID of the session.
462
+ * `subscribe(listener: (location) => {}): () => {}` — Subscribes to location changes, including setting the initial location. Returns an "unsubscribe" function. The `location` argument of the listener function is an "extended" location object having additional properties:
463
+ * `index: number` — The index of the location in the session's navigation history, starting with `0` for the initial location.
464
+ * `operation: string` — The type of navigation that led to the location.
465
+ * `INIT` in case of the initial location before any navigation has taken place.
466
+ * `SHIFT` when the user performs a "Back" or "Forward" navigation, or after a `.shift()` navigation which is essentially a "back or forward navigation".
467
+ * `PUSH` in case of a `.push()` navigation, i.e. "normal navigation via a hyperlink".
468
+ * `REPLACE` in case of a `.replace()` navigation, i.e. "redirect".
469
+ * `delta: number` — the difference between the `index` of the current location and the `index` of the previous location.
470
+ * `0` for the initial location before any navigation has taken place.
471
+ * `1` after a `.push()` navigation, i.e. "normal navigation via a hyperlink".
472
+ * `0` after a `.replace()` navigation, i.e. "redirect".
473
+ * `delta: number` after a `.shift(delta)` navigation, i.e. "back or forward navigation".
474
+ * `-1` after the user clicks a "Back" button in their web browser.
475
+ * `1` after the user clicks a "Forward" button in their web browser.
476
+ <!-- * `getInitialLocation(): object?` — Returns the initial location, if the session can get it from somewhere. For example, in a web browser, the initial location can be read from `window.location`. In other environments, such as server side, the initial location can't be read from anywhere. -->
477
+ * `start(initialLocation?: object)` — Starts the session.
478
+ * `stop()` — Stops the session. Cleans up any listeners, etc.
479
+ * `navigate(operation: string, location: object)` — Navigates to a `location` using either `"PUSH"` or `"REPLACE"` operation. The `location` argument should be a result of calling `parseInputLocation()` function.
480
+ * `shift(delta: number)` — Navigates "back" or "forward" by skipping a specified count of pages. Negative `delta` skips backwards, positive `delta` skips forward.
481
+ </details>
204
482
 
483
+ ## Utility
484
+
485
+ This package exports a few utility functions for transforming locations.
486
+
487
+ ```js
205
488
  import {
206
- createMiddlewares,
207
- locationReducer,
208
- Actions,
209
- BrowserSession,
210
- addBeforeLocationChangeListener
489
+ getLocationUrl,
490
+ parseLocationUrl,
491
+ parseInputLocation,
492
+ addBasePath,
493
+ removeBasePath
211
494
  } from 'navigation-stack'
212
495
 
213
- const session = new BrowserSession()
496
+ // The following two are "mutually inverse functions":
497
+ // one maps a `location` object to a URL string
498
+ // and the other maps a URL string to a `location` object.
214
499
 
215
- // Create a Redux store.
216
- const store = createStore(
217
- locationReducer, // Reducer function. For example, `locationReducer()`.
218
- applyMiddleware(...createMiddlewares(session))
219
- )
500
+ // Converts a location object to a location URL.
501
+ getLocationUrl({ pathname: '/abc', search: '?d=e' }) === '/abc?d=e'
220
502
 
221
- // Subscribe to "before location change" events.
222
- const removeBeforeLocationChangeListener = addBeforeLocationChangeListener(
223
- session,
224
- (newLocation) => {
225
- console.log(newLocation)
226
- }
227
- );
503
+ // Parses a location URL to a location object.
504
+ // If there're no query parameters, `query` property will be an empty object.
505
+ parseLocationUrl('/abc?d=e') === {
506
+ pathname: '/abc',
507
+ search: '?d=e',
508
+ query: { d: 'e' },
509
+ hash: ''
510
+ }
511
+
512
+ // The following function parses a non-strict location object to a strict one.
513
+ // It also parses a location URL to a location object.
514
+
515
+ parseInputLocation({ pathname: '/abc', search: '?d=e' }) === {
516
+ pathname: '/abc',
517
+ search: '?d=e',
518
+ query: { d: 'e' },
519
+ hash: ''
520
+ }
521
+
522
+ parseInputLocation('/abc?d=e') === {
523
+ pathname: '/abc',
524
+ search: '?d=e',
525
+ query: { d: 'e' },
526
+ hash: ''
527
+ }
228
528
 
229
- // Initialize navigation.
230
- // This will not trigger the listener because it's not a navigation.
231
- store.dispatch(Actions.init())
529
+ // The following two functions can be used to add base path to a location
530
+ // or to remove it from it.
232
531
 
233
- // This navigation event will trigger the listener.
234
- // `newLocation.action` will be "PUSH".
235
- store.dispatch(Actions.push('/new/location'))
532
+ // Adds `basePath` to a location object or a location URL.
533
+ addBasePath('/abc', '/base-path') === '/base-path/abc'
534
+ addBasePath({ pathname: '/abc' }, '/base-path') === { pathname: '/base-path/abc' }
236
535
 
237
- // Unsubscribe from "before location change" events.
238
- removeBeforeLocationChangeListener()
536
+ // Removes `basePath` from a location object or a location URL.
537
+ // If `basePath` is not present in location, it won't do anything.
538
+ removeBasePath('/base-path/abc', '/base-path') === '/abc';
539
+ removeBasePath({ pathname: '/base-path/abc' }, '/base-path') === { pathname: '/abc' }
239
540
  ```
240
541
 
241
542
  ## Block Navigation
242
543
 
243
- `navigation-stack` provides the ability to block navigation. Call `addNavigationBlocker()` exported function to set up a navigation blocker.
544
+ `navigation-stack` provides the ability to block navigation. Call `addNavigationBlocker()` function to set up a "navigation blocker".
244
545
 
245
546
  ```js
246
- import { createStore, applyMiddleware } from 'redux'
247
-
248
547
  import {
249
- createMiddlewares,
250
- locationReducer,
251
- Actions,
252
- BrowserSession,
548
+ NavigationStack,
549
+ WebBrowserSession,
253
550
  addNavigationBlocker
254
551
  } from 'navigation-stack'
255
552
 
256
- const session = new BrowserSession()
553
+ // Create a session.
554
+ const session = new WebBrowserSession()
257
555
 
258
- // Create a Redux store.
259
- const store = createStore(
260
- locationReducer, // Reducer function. For example, `locationReducer()`.
261
- applyMiddleware(...createMiddlewares(session))
262
- )
263
-
264
- // Initialize navigation.
265
- store.dispatch(Actions.init())
556
+ // Create a `NavigationStack` instance.
557
+ const navigationStack = new NavigationStack(session)
266
558
 
267
- // Add navigation blocker.
559
+ // Add a navigation blocker.
560
+ // It should be tied to the same "session".
268
561
  const removeNavigationBlocker = addNavigationBlocker(
269
562
  session,
270
563
  (newLocation) => {
271
- // Returning `true` means "block this navigation".
564
+ // Returning `true` means "this navigation should be blocked".
272
565
  return true
273
566
  }
274
567
  );
275
568
 
276
- // This navigation won't be performed.
277
- store.dispatch(Actions.push('/new/location'))
569
+ // Because the navigation is blocked, current location will not change here.
570
+ //
571
+ // The URL in the web browser's address bar will stay the same
572
+ // and no new entries will be added in the web browser's navigation history.
573
+ //
574
+ navigationStack.push('/new-location')
278
575
 
279
576
  // Remove the navigation blocker.
280
577
  removeNavigationBlocker()
281
578
 
282
- // This navigation now will be performed.
283
- store.dispatch(Actions.push('/new/location'))
579
+ // With the blocker removed, current location will be set to a new one.
580
+ //
581
+ // This also updates the URL in the web browser's address bar
582
+ // and adds a new entry in the web browser's navigation history.
583
+ //
584
+ navigationStack.push('/new-location')
284
585
  ```
285
586
 
286
587
  Navigation blocker should be a function that receives a `newLocation` argument and could be "synchronous" or "asynchronous" (i.e. return a `Promise`, aka `async`/`await`).
287
588
 
288
- The `newLocation` argument of a blocker function won't necessarily have a `key` or `index` property but other properties are present.
589
+ The `newLocation` argument of a blocker function won't necessarily have a `key` property but other properties are present.
289
590
 
290
- Navigation blockers fire both when navigating from one page to another and when closing the current browser tab. In the latter case, `newLocation` argument will be `null`, the function can't return a `Promise`, and returning `true` will cause the web browser to show a confirmation modal with a non-customizable browser-specific text.
591
+ Navigation blockers fire both when navigating from one page to another and when closing the current browser tab. In the latter case, `newLocation` argument will be `null`, and also the blocker function can't return a `Promise` (because it won't wait), and returning `true` from it will cause the web browser will to show a confirmation modal with a non-customizable generic browser-specific text like "Leave site? Changes you made might not be saved".
291
592
 
292
- ## Utility
593
+ ## Data Storage
594
+
595
+ One could use `DataStorage` to store any kind of application-specific data in a given "session". The data will exist as long as the "session" exists.
596
+
597
+ Different types of data could be stored under a different `key`.
598
+
599
+ If each different location should have it's own data stored under the same `key`, one could use `LocationDataStorage` instead of just `DataStorage`. For example, one could store scroll position for each different page to be able to restore it when the user decides to navigate "Back" to that page. By the way, that's precisely what `ScrollPositionRestoration` does.
293
600
 
294
- This package exports a couple of utility functions.
601
+ `DataStorage` constructor receives a `session` argument and a `namespace` parameter. The `namespace` just gets prepended to every `key`. The idea is that your `namespace` must not clash with anyone else's `namespace` who might potentially use the same `session` to store their own data.
295
602
 
296
603
  ```js
604
+ import { WebBrowserSession } from 'navigation-stack'
605
+ import { DataStorage, LocationDataStorage } from 'navigation-stack/data-storage'
606
+
607
+ const session = new WebBrowserSession()
608
+
609
+ // `DataStorage` example
610
+
611
+ const dataStorage = new DataStorage(session, { namespace: 'my-namespace' })
612
+
613
+ dataStorage.set('key', 123)
614
+ dataStorage.get('key') === 123
615
+
616
+ // `LocationDataStorage` example
617
+
618
+ const locationDataStorage = new LocationDataStorage(session, { namespace: 'my-namespace' })
619
+
620
+ const location = { pathname: '/abc' }
621
+
622
+ locationDataStorage.set(location, 'key', 123)
623
+ locationDataStorage.get(location, 'key') === 123
624
+ ```
625
+
626
+ `DataStorage` or `LocationDataStorage` don't provide any guarantees about actually storing the data: if it encounters any errors in the process, it simply ignores them. This simplifies the API in a way that the application doesn't have to wrap `.get()`/`.set()` calls in a `try/catch` block. And judging by the nature of location-specific data, that type of data is inherently non-essential and rather "nice-to-have".
627
+
628
+ One might ask: Why use `DataStorage` or `LocationDataStorage` when one could simply store the data in a usual variable? The answer is that a usual variable doesn't survive if the user decides to refresh the page. But the entire navigation history does survive because that's how web browsers work. So if the user decides to go "Back" after refreshing the current page, the data associated to that previous location would already be lost and can't be recovered. In contrast, when using a `DataStorage` or `LocationDataStorage` with a `WebBrowserSession`, the stored data does survive a page refresh, which feels more consistent and coherent with the persistence behavior of the navigation history itself.
629
+
630
+ ## Redux
631
+
632
+ Under the hood, `navigation-stack` uses [`redux`](https://redux.js.org/). Why? For no particular reason. The original [`farce`](http://npmjs.com/package/farce) package was published in September 2016, and by that time `redux` had still been a hot topic since [July 2025](https://www.youtube.com/watch?v=xsSnOQynTHs). This package could most certainly be rewritten without using `redux`, it's just that there seems to be no need to do that.
633
+
634
+ So since `navigation-stack` already implements all that `redux` stuff internally, such as "middlewares" or "actions", why not export it for public usage? Maybe there're still some `redux` fans out there.
635
+
636
+ Using `navigation-stack` `redux`-way is equivalent to using it the conventional way via `NavigationStack` class. `navigation-stack` exports "middlewares", "actions" and a "reducer" that could be used in conjunction with `redux` or any other `redux`-compatible package (e.g. [`mini-redux`](https://www.npmjs.com/package/mini-redux)).
637
+
638
+ <details>
639
+ <summary>See <code>redux</code>-style API</summary>
640
+
641
+ ######
642
+
643
+ Start by creating a Redux "store" with `navigation-stack` middlewares.
644
+
645
+ ```js
646
+ import { createStore, applyMiddleware } from 'redux';
647
+
297
648
  import {
298
- addBasePath,
299
- removeBasePath,
300
- getLocationUrl,
301
- parseLocationUrl
302
- } from 'navigation-stack'
649
+ createMiddlewares,
650
+ locationReducer,
651
+ Actions,
652
+ WebBrowserSession,
653
+ } from 'navigation-stack';
303
654
 
304
- // Parses a location URL to a location object.
305
- // If there're no query parameters, `query` property will be an empty object.
306
- parseLocationUrl('/abc?d=e') === {
307
- pathname: '/abc',
308
- search: '?d=e',
309
- query: { d: 'e' },
310
- hash: ''
311
- }
655
+ // Create a Redux store.
656
+ const store = createStore(
657
+ // Reducer function. For example, `locationReducer()`.
658
+ locationReducer,
659
+ // It should be tied to a navigation "session".
660
+ applyMiddleware(...createMiddlewares(new WebBrowserSession())),
661
+ );
662
+ ```
312
663
 
313
- // Converts a location object to a location URL.
314
- getLocationUrl({ pathname: '/abc', search: '?d=e', hash: '' }) === '/abc?d=e'
664
+ Next, set the initial location. Normally, `NavigationStack` class API automatically performs this step for a developer. When using Redux API though, it doesn't do that and the developer has to do it themself.
315
665
 
316
- // Adds `basePath` to a location object or a location URL.
317
- addBasePath('/abc', '/base-path') === '/base-path/abc'
318
- addBasePath({ pathname: '/abc' }, '/base-path') === { pathname: '/base-path/abc' }
666
+ ```js
667
+ // Sets the initial `location`.
668
+ //
669
+ // Accepts either a relative URL string or a location object.
670
+ //
671
+ // The initial location argument could be omitted for `WebBrowserSession`
672
+ // because it can read it by itself from `window.location`.
673
+ // Other types of session such as `InMemorySession` or `ServerSideRenderSession`
674
+ // don't have an initial location and require the initial location argument
675
+ // to be specified explicitly when creating an `Actions.init(initialLocation)` action.
676
+ //
677
+ store.dispatch(Actions.init());
678
+ ```
319
679
 
320
- // Removes `basePath` from a location object or a location URL.
321
- // If `basePath` is not present in location, it won't do anything.
322
- removeBasePath('/base-path/abc', '/base-path') === '/abc';
323
- removeBasePath({ pathname: '/base-path/abc' }, '/base-path') === { pathname: '/abc' }
680
+ Then subscribe to location changes. One could use Redux'es standard [subscription mechanisms](https://redux.js.org/api/store#subscribelistener) to immediately get notified of current location changes.
681
+
682
+ ```js
683
+ let currentLocation;
684
+
685
+ // Create a Redux store.
686
+ const store = createStore(
687
+ locationReducer, // Reducer function. For example, `locationReducer()`.
688
+ applyMiddleware(...createMiddlewares(new WebBrowserSession())),
689
+ );
690
+
691
+ // Subscribe to any potential Redux state changes.
692
+ const unsubscribe = store.subscribe(() => {
693
+ const previousLocation = currentLocation;
694
+ currentLocation = store.getState(); // In case of using `locationReducer()`.
695
+ if (currentLocation !== previousLocation) {
696
+ console.log('Current location', currentLocation);
697
+ }
698
+ });
699
+ ```
700
+
701
+ Now ready to perform navigation actions by dispatching any of the available `Actions`.
702
+
703
+ ```js
704
+ // Sets the `location` to be a new location.
705
+ //
706
+ // Also updates the URL in the web browser's address bar.
707
+ //
708
+ // Also adds a new entry in the web browser's navigation history.
709
+ //
710
+ store.dispatch(Actions.push('/new-location'));
711
+
712
+ // Sets the `location` to be a new location.
713
+ //
714
+ // Also updates the URL in the web browser's address bar.
715
+ //
716
+ // Does not add a new entry in the web browser's navigation history
717
+ // which is the only difference between this and `Actions.push()`.
718
+ //
719
+ store.dispatch(Actions.replace('/new-location'));
720
+
721
+ // Sets the `location` to be a previous one (if there is one).
722
+ // One could think of it as an equivalent of clicking a "Back" button in a web browser.
723
+ //
724
+ // Also updates the URL in the web browser's address bar.
725
+ //
726
+ // Also shifts the current position in the web browser's navigation history.
727
+ //
728
+ store.dispatch(Actions.shift(-1));
729
+
730
+ // Sets the `location` to be a next one (if there is one).
731
+ // One could think of it as an equivalent of clicking a "Forward" button in a web browser.
732
+ //
733
+ // Also updates the URL in the web browser's address bar.
734
+ //
735
+ // Also shifts the current position in the web browser's navigation history.
736
+ //
737
+ store.dispatch(Actions.shift(1));
324
738
  ```
325
739
 
740
+ To get the current location:
741
+
742
+ ```js
743
+ // When `locationReducer()` is used, `store.getState()` returns the current location.
744
+ const location = store.getState();
745
+ console.log(location);
746
+ ```
747
+
748
+ (optional) After the user is done using the app, stop the session and clean up any listeners.
749
+
750
+ ```js
751
+ // (optional)
752
+ // When the user closes the application,
753
+ // stop the session and clean up any listeners.
754
+ // There's no need to do this in a web browser.
755
+ unsubscribe();
756
+ store.dispatch(Actions.stop());
757
+ ```
758
+ </details>
759
+
326
760
  ## Development
327
761
 
328
762
  Clone the repository. Then:
@@ -332,3 +766,9 @@ yarn
332
766
  yarn format
333
767
  yarn test
334
768
  ```
769
+
770
+ It runs tests in two web browsers (for no particular reason) — Chrome and Firefox (configurable in `karma.conf.cjs`). When running `yarn test`, it opens Chome and Firefox browser windows. Don't unfocus those windows, otherwise the tests won't finish.
771
+
772
+ ## GitHub
773
+
774
+ On March 9th, 2020, GitHub, Inc. silently [banned](https://medium.com/@catamphetamine/how-github-blocked-me-and-all-my-libraries-c32c61f061d3) my account (erasing all my repos, issues and comments, even in my employer's private repos) without any notice or explanation. Because of that, all source codes had to be promptly moved to GitLab. The [GitHub repo](https://github.com/catamphetamine/navigation-stack) is now only used as a backup (you can star the repo there too), and the primary repo is now the [GitLab one](https://gitlab.com/catamphetamine/navigation-stack). Issues can be reported in any repo.