navigation-stack 0.3.1 → 0.5.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.
- package/CHANGELOG.md +12 -0
- package/README.md +611 -163
- package/data-storage/package.json +6 -0
- package/karma.conf.cjs +21 -4
- package/lib/cjs/NavigationStack.js +88 -0
- package/lib/cjs/data-storage/DataStorage.js +71 -0
- package/lib/cjs/data-storage/LocationDataStorage.js +29 -0
- package/lib/cjs/data-storage/index.js +9 -0
- package/lib/cjs/debug.js +12 -0
- package/lib/cjs/environment/InMemoryEnvironment.js +15 -0
- package/lib/cjs/environment/WebBrowserEnvironment.js +15 -0
- package/lib/cjs/environment/data-storage/InMemoryDataStorage.js +27 -0
- package/lib/cjs/environment/data-storage/WebBrowserDataStorage.js +21 -0
- package/lib/cjs/environment/scroll-position/InMemoryScrollPosition.js +44 -0
- package/lib/cjs/environment/scroll-position/WebBrowserScrollPosition.js +60 -0
- package/lib/cjs/getLocationFromInternalLocation.js +14 -0
- package/lib/cjs/index.js +20 -16
- package/lib/cjs/navigationBlockers.js +28 -23
- package/lib/cjs/{normalizeInputLocation.js → parseInputLocation.js} +25 -9
- package/lib/cjs/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/lib/cjs/redux/ActionTypesInternal.js +8 -0
- package/lib/cjs/{Actions.js → redux/Actions.js} +5 -4
- package/lib/cjs/redux/createMiddlewares.js +60 -0
- package/lib/cjs/redux/index.js +13 -0
- package/lib/cjs/redux/internalLocationReducer.js +14 -0
- package/lib/cjs/redux/middleware/createAddInputLocationBasePathMiddleware.js +32 -0
- package/lib/cjs/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +113 -0
- package/lib/cjs/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
- package/lib/cjs/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +30 -0
- package/lib/cjs/redux/middleware/createUpdateInternalLocationMiddleware.js +73 -0
- package/lib/cjs/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +11 -8
- package/lib/cjs/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +6 -4
- package/lib/cjs/redux/middleware/updateLocationMiddleware.js +34 -0
- package/lib/cjs/scroll-position/PageScrollPositionSetter.js +97 -0
- package/lib/cjs/scroll-position/ScrollPositionAutoSaver.js +141 -0
- package/lib/cjs/scroll-position/ScrollPositionRestoration.js +407 -0
- package/lib/cjs/scroll-position/ScrollPositionSaver.js +87 -0
- package/lib/cjs/scroll-position/ScrollPositionSetter.js +16 -0
- package/lib/cjs/scroll-position/constants.js +5 -0
- package/lib/cjs/scroll-position/index.js +7 -0
- package/lib/cjs/scroll-position/scheduleNextTick.js +11 -0
- package/lib/cjs/session/InMemorySession.js +22 -0
- package/lib/cjs/session/ServerSideRenderSession.js +17 -0
- package/lib/cjs/session/Session.js +202 -0
- package/lib/cjs/session/WebBrowserSession.js +20 -0
- package/lib/cjs/session/key/createSessionKey.js +23 -0
- package/lib/cjs/session/lifecycle/InMemorySessionLifecycle.js +19 -0
- package/lib/cjs/session/lifecycle/WebBrowserSessionLifecycle.js +128 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycle.js +269 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +8 -0
- package/lib/cjs/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +33 -0
- package/lib/cjs/session/navigation/InMemoryNavigation.js +104 -0
- package/lib/cjs/session/navigation/ServerSideNavigation.js +61 -0
- package/lib/cjs/session/navigation/WebBrowserNavigation.js +221 -0
- package/lib/cjs/session/navigation/error/NavigationOutOfBoundsError.js +12 -0
- package/lib/cjs/session/navigation/error/ServerSideNavigationError.js +21 -0
- package/lib/cjs/session/navigation/operation/operations.js +11 -0
- package/lib/cjs/session/subscription/Subscription.js +81 -0
- package/lib/data-storage/index.d.ts +35 -0
- package/lib/esm/NavigationStack.js +81 -0
- package/lib/esm/data-storage/DataStorage.js +65 -0
- package/lib/esm/data-storage/LocationDataStorage.js +22 -0
- package/lib/esm/data-storage/index.js +2 -0
- package/lib/esm/debug.js +7 -0
- package/lib/esm/environment/InMemoryEnvironment.js +8 -0
- package/lib/esm/environment/WebBrowserEnvironment.js +8 -0
- package/lib/esm/environment/data-storage/InMemoryDataStorage.js +21 -0
- package/lib/esm/environment/data-storage/WebBrowserDataStorage.js +15 -0
- package/lib/esm/environment/scroll-position/InMemoryScrollPosition.js +38 -0
- package/lib/esm/environment/scroll-position/WebBrowserScrollPosition.js +54 -0
- package/lib/esm/getLocationFromInternalLocation.js +9 -0
- package/lib/esm/index.js +10 -8
- package/lib/esm/navigationBlockers.js +28 -23
- package/lib/esm/{normalizeInputLocation.js → parseInputLocation.js} +24 -8
- package/lib/esm/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/lib/esm/redux/ActionTypesInternal.js +3 -0
- package/lib/esm/{Actions.js → redux/Actions.js} +5 -4
- package/lib/esm/redux/createMiddlewares.js +54 -0
- package/lib/esm/redux/index.js +4 -0
- package/lib/esm/redux/internalLocationReducer.js +8 -0
- package/lib/esm/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
- package/lib/esm/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +108 -0
- package/lib/esm/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +88 -0
- package/lib/esm/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +25 -0
- package/lib/esm/redux/middleware/createUpdateInternalLocationMiddleware.js +68 -0
- package/lib/esm/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -7
- package/lib/esm/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
- package/lib/esm/redux/middleware/updateLocationMiddleware.js +28 -0
- package/lib/esm/scroll-position/PageScrollPositionSetter.js +91 -0
- package/lib/esm/scroll-position/ScrollPositionAutoSaver.js +134 -0
- package/lib/esm/scroll-position/ScrollPositionRestoration.js +400 -0
- package/lib/esm/scroll-position/ScrollPositionSaver.js +80 -0
- package/lib/esm/scroll-position/ScrollPositionSetter.js +10 -0
- package/lib/esm/scroll-position/constants.js +1 -0
- package/lib/esm/scroll-position/index.js +1 -0
- package/lib/esm/scroll-position/scheduleNextTick.js +6 -0
- package/lib/esm/session/InMemorySession.js +15 -0
- package/lib/esm/session/ServerSideRenderSession.js +11 -0
- package/lib/esm/session/Session.js +195 -0
- package/lib/esm/session/WebBrowserSession.js +13 -0
- package/lib/esm/session/key/createSessionKey.js +18 -0
- package/lib/esm/session/lifecycle/InMemorySessionLifecycle.js +13 -0
- package/lib/esm/session/lifecycle/WebBrowserSessionLifecycle.js +120 -0
- package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycle.js +263 -0
- package/lib/esm/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +2 -0
- package/lib/esm/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +30 -0
- package/lib/esm/session/navigation/InMemoryNavigation.js +97 -0
- package/lib/esm/session/navigation/ServerSideNavigation.js +54 -0
- package/lib/esm/session/navigation/WebBrowserNavigation.js +213 -0
- package/lib/esm/session/navigation/error/NavigationOutOfBoundsError.js +6 -0
- package/lib/esm/session/navigation/error/ServerSideNavigationError.js +14 -0
- package/lib/esm/session/navigation/operation/operations.js +6 -0
- package/lib/esm/session/subscription/Subscription.js +75 -0
- package/lib/index.d.ts +179 -157
- package/lib/redux/index.d.ts +90 -0
- package/lib/scroll-position/index.d.ts +107 -0
- package/package.json +9 -5
- package/redux/package.json +6 -0
- package/scroll-position/package.json +6 -0
- package/src/NavigationStack.js +100 -0
- package/src/data-storage/DataStorage.js +69 -0
- package/src/data-storage/LocationDataStorage.js +23 -0
- package/src/data-storage/index.js +2 -0
- package/src/debug.js +8 -0
- package/src/environment/InMemoryEnvironment.js +9 -0
- package/src/environment/WebBrowserEnvironment.js +9 -0
- package/src/environment/data-storage/InMemoryDataStorage.js +23 -0
- package/src/environment/data-storage/WebBrowserDataStorage.js +17 -0
- package/src/environment/scroll-position/InMemoryScrollPosition.js +45 -0
- package/src/environment/scroll-position/WebBrowserScrollPosition.js +72 -0
- package/src/getLocationFromInternalLocation.js +7 -0
- package/src/index.js +10 -8
- package/src/navigationBlockers.js +31 -27
- package/src/{normalizeInputLocation.js → parseInputLocation.js} +23 -8
- package/src/{ActionTypes.js → redux/ActionTypes.js} +1 -1
- package/src/redux/ActionTypesInternal.js +3 -0
- package/src/{Actions.js → redux/Actions.js} +4 -3
- package/src/redux/createMiddlewares.js +65 -0
- package/src/redux/index.js +4 -0
- package/src/redux/internalLocationReducer.js +9 -0
- package/src/redux/middleware/createAddInputLocationBasePathMiddleware.js +27 -0
- package/src/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.js +119 -0
- package/src/redux/middleware/createProgrammaticNavigationBlockerMiddleware.js +94 -0
- package/src/redux/middleware/createRemoveOutputLocationBasePathMiddleware.js +26 -0
- package/src/redux/middleware/createUpdateInternalLocationMiddleware.js +72 -0
- package/src/{middleware/navigationActionMiddleware.js → redux/middleware/navigationOperationMiddleware.js} +10 -3
- package/src/{middleware/normalizeInputLocationMiddleware.js → redux/middleware/parseInputLocationMiddleware.js} +5 -3
- package/src/redux/middleware/updateLocationMiddleware.js +28 -0
- package/src/scroll-position/PageScrollPositionSetter.js +110 -0
- package/src/scroll-position/ScrollPositionAutoSaver.js +168 -0
- package/src/scroll-position/ScrollPositionRestoration.js +551 -0
- package/src/scroll-position/ScrollPositionSaver.js +120 -0
- package/src/scroll-position/ScrollPositionSetter.js +16 -0
- package/src/scroll-position/constants.js +1 -0
- package/src/scroll-position/index.js +1 -0
- package/src/scroll-position/scheduleNextTick.js +6 -0
- package/src/session/InMemorySession.js +13 -0
- package/src/session/ServerSideRenderSession.js +9 -0
- package/src/session/Session.js +238 -0
- package/src/session/WebBrowserSession.js +13 -0
- package/src/session/key/createSessionKey.js +18 -0
- package/src/session/lifecycle/InMemorySessionLifecycle.js +13 -0
- package/src/session/lifecycle/WebBrowserSessionLifecycle.js +126 -0
- package/src/session/lifecycle/page-lifecycle/PageLifecycle.js +291 -0
- package/src/session/lifecycle/page-lifecycle/PageLifecycleInstance.js +3 -0
- package/src/session/lifecycle/page-lifecycle/supportsConstructableEventTarget.js +32 -0
- package/src/session/navigation/InMemoryNavigation.js +78 -0
- package/src/session/navigation/ServerSideNavigation.js +43 -0
- package/src/session/navigation/WebBrowserNavigation.js +224 -0
- package/src/session/navigation/error/NavigationOutOfBoundsError.js +7 -0
- package/src/session/navigation/error/ServerSideNavigationError.js +18 -0
- package/src/session/navigation/operation/operations.js +6 -0
- package/src/session/subscription/Subscription.js +76 -0
- package/test/NavigationStack.test.js +296 -0
- package/test/{LocationDataStorage.test.js → data-storage/LocationDataStorage.test.js} +3 -3
- package/test/data-storage/index.test.js +8 -0
- package/test/index.js +12 -0
- package/test/index.test.js +8 -7
- package/test/{helpers.js → middlewareTestUtil.js} +9 -12
- package/test/{normalizeInputLocation.test.js → parseInputLocationMiddleware.test.js} +9 -9
- package/test/{Action.test.js → redux/Action.test.js} +7 -6
- package/test/{ActionTypes.test.js → redux/ActionTypes.test.js} +2 -2
- package/test/redux/createMiddlewares.test.js +96 -0
- package/test/redux/index.test.js +10 -0
- package/test/{locationReducer.test.js → redux/locationReducer.test.js} +4 -7
- package/test/redux/middleware/createAddInputLocationBasePathMiddleware.test.js +40 -0
- package/test/redux/middleware/createNonProgrammaticNavigationBlockerMiddleware.test.js +264 -0
- package/test/redux/middleware/createProgrammaticNavigationBlockerMiddleware.test.js +312 -0
- package/test/redux/middleware/createRemoveOutputLocationBasePathMiddleware.test.js +51 -0
- package/test/{middleware/navigationActionMiddleware.test.js → redux/middleware/navigationOperationMiddleware.test.js} +16 -12
- package/test/{middleware/normalizeInputLocationMiddleware.test.js → redux/middleware/parseInputLocationMiddleware.test.js} +4 -4
- package/test/scroll-position/ScrollPositionRestoration.test.js +435 -0
- package/test/scroll-position/addScrollableContainer.js +39 -0
- package/test/scroll-position/addScrollableContainerWithAnchors.js +56 -0
- package/test/scroll-position/createApp.js +132 -0
- package/test/scroll-position/delay.js +9 -0
- package/test/scroll-position/mockPageLifecycle.js +17 -0
- package/test/scroll-position/runApp.js +24 -0
- package/test/scroll-position/withScrollableContainerAtIndexPageWithDisabledAutomaticScrollPositionRestoration.js +72 -0
- package/test/session/InMemorySession.test.js +348 -0
- package/test/session/ServerSession.test.js +17 -9
- package/test/session/WebBrowserSession.test.js +265 -0
- package/test/testUtil.js +3 -0
- package/types/data-storage/index.d.ts +35 -0
- package/types/index.d.ts +179 -157
- package/types/redux/index.d.ts +90 -0
- package/types/scroll-position/index.d.ts +107 -0
- package/types/tsconfig.json +1 -1
- package/lib/cjs/LocationDataStorage.js +0 -61
- package/lib/cjs/addBeforeLocationChangeListener.js +0 -7
- package/lib/cjs/beforeLocationChangeListeners.js +0 -51
- package/lib/cjs/createMiddlewares.js +0 -47
- package/lib/cjs/middleware/createBasePathMiddleware.js +0 -24
- package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -39
- package/lib/cjs/middleware/createLocationMiddleware.js +0 -56
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +0 -161
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +0 -38
- package/lib/cjs/onlyAllowedOnClientSide.js +0 -10
- package/lib/cjs/session/BrowserSession.js +0 -235
- package/lib/cjs/session/MemorySession.js +0 -223
- package/lib/cjs/session/ServerSession.js +0 -65
- package/lib/esm/LocationDataStorage.js +0 -54
- package/lib/esm/addBeforeLocationChangeListener.js +0 -2
- package/lib/esm/beforeLocationChangeListeners.js +0 -44
- package/lib/esm/createMiddlewares.js +0 -41
- package/lib/esm/middleware/createBasePathMiddleware.js +0 -19
- package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -34
- package/lib/esm/middleware/createLocationMiddleware.js +0 -50
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +0 -156
- package/lib/esm/middleware/createTransformLocationMiddleware.js +0 -33
- package/lib/esm/onlyAllowedOnClientSide.js +0 -5
- package/lib/esm/session/BrowserSession.js +0 -229
- package/lib/esm/session/MemorySession.js +0 -217
- package/lib/esm/session/ServerSession.js +0 -58
- package/src/LocationDataStorage.js +0 -60
- package/src/addBeforeLocationChangeListener.js +0 -2
- package/src/beforeLocationChangeListeners.js +0 -54
- package/src/createMiddlewares.js +0 -45
- package/src/middleware/createBasePathMiddleware.js +0 -20
- package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +0 -40
- package/src/middleware/createLocationMiddleware.js +0 -55
- package/src/middleware/createNavigationBlockerMiddleware.js +0 -168
- package/src/middleware/createTransformLocationMiddleware.js +0 -29
- package/src/onlyAllowedOnClientSide.js +0 -5
- package/src/session/BrowserSession.js +0 -235
- package/src/session/MemorySession.js +0 -219
- package/src/session/ServerSession.js +0 -67
- package/test/createMiddlewares.test.js +0 -62
- package/test/middleware/createBasePathMiddleware.test.js +0 -67
- package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +0 -141
- package/test/middleware/createNavigationBlockerMiddleware.test.js +0 -471
- package/test/middleware/createTransformLocationMiddleware.test.js +0 -44
- package/test/session/BrowserSession.test.js +0 -182
- package/test/session/MemorySession.test.js +0 -244
- /package/lib/cjs/{locationReducer.js → redux/locationReducer.js} +0 -0
- /package/lib/esm/{locationReducer.js → redux/locationReducer.js} +0 -0
- /package/src/{locationReducer.js → redux/locationReducer.js} +0 -0
package/README.md
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/navigation-stack)
|
|
4
4
|
[](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
|
|
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
|
-
|
|
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,99 @@ npm install navigation-stack
|
|
|
15
17
|
|
|
16
18
|
## Use
|
|
17
19
|
|
|
18
|
-
`
|
|
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
|
-
|
|
24
|
-
createMiddlewares,
|
|
25
|
-
locationReducer,
|
|
26
|
-
Actions,
|
|
27
|
-
BrowserSession
|
|
28
|
-
} from 'navigation-stack'
|
|
22
|
+
Start by creating a `NavigationStack` instance.
|
|
29
23
|
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
//
|
|
37
|
-
|
|
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
|
-
|
|
32
|
+
Then subscribe to changes:
|
|
41
33
|
|
|
42
34
|
```js
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
store.dispatch(Actions.shift(-1))
|
|
44
|
+
Now ready to perform navigation actions.
|
|
51
45
|
|
|
52
|
-
|
|
53
|
-
|
|
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,
|
|
69
|
+
// throws a `NavigationOutOfBoundsError` error that has an `index` property.
|
|
70
|
+
//
|
|
71
|
+
// One could think of it as an equivalent of clicking a "Back" button in a web browser.
|
|
72
|
+
//
|
|
73
|
+
// Also updates the URL in the web browser's address bar.
|
|
74
|
+
//
|
|
75
|
+
// Also shifts the current position in the web browser's navigation history.
|
|
76
|
+
//
|
|
77
|
+
navigationStack.shift(-1)
|
|
78
|
+
|
|
79
|
+
// Sets the `location` to be a next one (if there is one).
|
|
80
|
+
// If there's no such `location` in the navigation history,
|
|
81
|
+
// throws a `NavigationOutOfBoundsError` error that has an `index` property.
|
|
82
|
+
//
|
|
83
|
+
// One could think of it as an equivalent of clicking a "Forward" button in a web browser.
|
|
84
|
+
//
|
|
85
|
+
// Also updates the URL in the web browser's address bar.
|
|
86
|
+
//
|
|
87
|
+
// Also shifts the current position in the web browser's navigation history.
|
|
88
|
+
//
|
|
89
|
+
navigationStack.shift(1)
|
|
54
90
|
```
|
|
55
91
|
|
|
56
|
-
To
|
|
92
|
+
To get the current location:
|
|
57
93
|
|
|
58
94
|
```js
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
console.log(store.getState())
|
|
95
|
+
const location = navigationStack.current()
|
|
96
|
+
console.log(location)
|
|
62
97
|
```
|
|
63
98
|
|
|
64
|
-
(optional)
|
|
99
|
+
(optional) After the user is done using the app, stop the session and clean up any listeners.
|
|
65
100
|
|
|
66
101
|
```js
|
|
67
|
-
|
|
102
|
+
// (optional)
|
|
103
|
+
// When the user closes the application,
|
|
104
|
+
// stop the session and clean up any listeners.
|
|
105
|
+
// There's no need to do this in a web browser.
|
|
106
|
+
unsubscribe()
|
|
107
|
+
navigationStack.stop()
|
|
68
108
|
```
|
|
69
109
|
|
|
70
110
|
## Current Location
|
|
71
111
|
|
|
112
|
+
<!--
|
|
72
113
|
To track the current location, the application could listen to `ActionTypes.UPDATE` action. The `payload` of the action is the current location.
|
|
73
114
|
|
|
74
115
|
For example, below is the source code for the default `locationReducer`.
|
|
@@ -88,27 +129,19 @@ function reducer(state, action) {
|
|
|
88
129
|
|
|
89
130
|
With this reducer, `store.getState()` will return the current location.
|
|
90
131
|
|
|
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.
|
|
132
|
+
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.
|
|
133
|
+
-->
|
|
92
134
|
|
|
93
|
-
|
|
135
|
+
To get the current location, use `navigationStack.current()`.
|
|
136
|
+
|
|
137
|
+
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
138
|
* `query: object` — URL query parameters.
|
|
95
|
-
* `
|
|
96
|
-
* `
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
139
|
+
* `key: string` — A string ID of the location that is guaranteed to be unique within the session's limits and could be used as a "key" to store any supplementary data associated to this location.
|
|
140
|
+
* `index: number` — The index of the location in the navigation stack, starting with `0` for the initial location.
|
|
141
|
+
|
|
142
|
+
<!-- ## Subscribe to Location Changes -->
|
|
111
143
|
|
|
144
|
+
<!--
|
|
112
145
|
One could use Redux'es standard [subscription mechanisms](https://redux.js.org/api/store#subscribelistener) to immediately get notified of current location changes.
|
|
113
146
|
|
|
114
147
|
```js
|
|
@@ -117,7 +150,7 @@ let currentLocation
|
|
|
117
150
|
// Create a Redux store.
|
|
118
151
|
const store = createStore(
|
|
119
152
|
locationReducer, // Reducer function. For example, `locationReducer()`.
|
|
120
|
-
applyMiddleware(...createMiddlewares(new
|
|
153
|
+
applyMiddleware(...createMiddlewares(new WebBrowserSession()))
|
|
121
154
|
)
|
|
122
155
|
|
|
123
156
|
// Subscribe to any potential Redux state changes.
|
|
@@ -125,204 +158,613 @@ const unsubscribe = store.subscribe(() => {
|
|
|
125
158
|
const previousLocation = currentLocation
|
|
126
159
|
currentLocation = store.getState() // In case of using `locationReducer()`.
|
|
127
160
|
if (currentLocation !== previousLocation) {
|
|
161
|
+
// The first time is for the initial location.
|
|
162
|
+
// Next times will happen in case of navigation.
|
|
128
163
|
console.log('Location has changed')
|
|
129
164
|
}
|
|
130
165
|
})
|
|
131
166
|
|
|
132
|
-
// Initialize navigation.
|
|
133
|
-
//
|
|
134
|
-
|
|
167
|
+
// Initialize navigation with an initial location.
|
|
168
|
+
//
|
|
169
|
+
// It will trigger the listener.
|
|
170
|
+
//
|
|
171
|
+
store.dispatch(Actions.init(window.location))
|
|
135
172
|
|
|
136
173
|
// Stop listening to current location changes.
|
|
137
174
|
unsubscribe()
|
|
138
175
|
```
|
|
176
|
+
-->
|
|
177
|
+
|
|
178
|
+
<!--
|
|
179
|
+
One could subscribe to location changes by calling `navigationStack.subscribe()`.
|
|
139
180
|
|
|
181
|
+
```js
|
|
182
|
+
// Create a `NavigationStack` instance.
|
|
183
|
+
// It should be tied to a navigation "session".
|
|
184
|
+
const navigationStack = new NavigationStack(new WebBrowserSession())
|
|
185
|
+
|
|
186
|
+
// Subscribe to location changes.
|
|
187
|
+
// The first call happens for the initial location.
|
|
188
|
+
// Next calls will happen in case of navigation.
|
|
189
|
+
const unsubscribe = navigationStack.subscribe((location) => {
|
|
190
|
+
console.log('Current location', location)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
// Navigate to a new location.
|
|
194
|
+
// It will trigger the listener.
|
|
195
|
+
navigationStack.push('/new-location')
|
|
196
|
+
|
|
197
|
+
// Stop listening to location changes.
|
|
198
|
+
unsubscribe()
|
|
199
|
+
```
|
|
200
|
+
-->
|
|
201
|
+
|
|
202
|
+
<!--
|
|
140
203
|
## Why Redux?
|
|
141
204
|
|
|
142
205
|
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
206
|
|
|
144
207
|
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.
|
|
208
|
+
-->
|
|
145
209
|
|
|
146
|
-
##
|
|
210
|
+
## Scroll Position Restoration
|
|
147
211
|
|
|
148
|
-
|
|
212
|
+
Pass `maintainScrollPosition: true` option to keep track of scroll position on every page and then automatically restore it on "Back" or "Forward" navigation.
|
|
149
213
|
|
|
150
214
|
```js
|
|
151
|
-
import {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
215
|
+
import { NavigationStack, WebBrowserSession } from 'navigation-stack'
|
|
216
|
+
|
|
217
|
+
// Create a `NavigationStack` instance with a `maintainScrollPosition: true` option.
|
|
218
|
+
const navigationStack = new NavigationStack(new WebBrowserSession(), {
|
|
219
|
+
maintainScrollPosition: true
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
//----------------------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
// Sets the initial location.
|
|
225
|
+
navigationStack.init()
|
|
226
|
+
|
|
227
|
+
// Render the initial location.
|
|
228
|
+
document.body.innerHTML = '<div> Initial Location </div>'
|
|
229
|
+
|
|
230
|
+
// As soon as a page has been rendered, without any delay, tell `NavigationStack` to restore
|
|
231
|
+
// a previously-saved scroll position, if there's any.
|
|
232
|
+
//
|
|
233
|
+
// This method must be called both for the initial location and any subsequent location.
|
|
234
|
+
//
|
|
235
|
+
navigationStack.locationRendered()
|
|
236
|
+
|
|
237
|
+
//----------------------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
// Set the `location` to be a new location.
|
|
240
|
+
//
|
|
241
|
+
// This also updates the URL in the web browser's address bar
|
|
242
|
+
// and adds a new entry in the web browser's navigation history.
|
|
243
|
+
//
|
|
244
|
+
navigationStack.push('/new-location')
|
|
245
|
+
|
|
246
|
+
// Render the new location.
|
|
247
|
+
document.body.innerHTML = '<div> New Location </div>'
|
|
156
248
|
|
|
157
|
-
new
|
|
158
|
-
|
|
159
|
-
|
|
249
|
+
// The new location is now rendered.
|
|
250
|
+
// Immediately after it has been rendered, call `.locationRenered()`.
|
|
251
|
+
// There's no scroll position to restore because it's not a previously-visited location.
|
|
252
|
+
navigationStack.locationRendered()
|
|
253
|
+
|
|
254
|
+
//----------------------------------------------------------------------------------------
|
|
255
|
+
|
|
256
|
+
// Set `location` "back" to the initial location.
|
|
257
|
+
//
|
|
258
|
+
// This also updates the URL in the web browser's address bar
|
|
259
|
+
// and repositions the "current location" pointer in the web browser's navigation history.
|
|
260
|
+
//
|
|
261
|
+
navigationStack.shift(-1)
|
|
262
|
+
|
|
263
|
+
// Render the initial location.
|
|
264
|
+
document.body.innerHTML = '<div> Initial Location </div>'
|
|
265
|
+
|
|
266
|
+
// The initial location is now rendered.
|
|
267
|
+
// Immediately after it has been rendered, call `.locationRenered()`.
|
|
268
|
+
// Restores the scroll position at the initial location.
|
|
269
|
+
navigationStack.locationRendered()
|
|
270
|
+
|
|
271
|
+
//----------------------------------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
// (optional)
|
|
274
|
+
// When the user is about to close the application,
|
|
275
|
+
// stop the `NavigationStack` and clean up any of its listeners.
|
|
276
|
+
// This is not required in a web browser because it cleans up all listeners
|
|
277
|
+
// automatically when closing a tab.
|
|
278
|
+
navigationStack.stop()
|
|
160
279
|
```
|
|
161
280
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
281
|
+
`NavigationStack` provides methods:
|
|
282
|
+
|
|
283
|
+
* `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.
|
|
284
|
+
* `locationRendered()` — Call it every time a different location has been rendered, including the initial location, without any delay, i.e. immediately after a different location has been rendered.
|
|
285
|
+
|
|
286
|
+
<details>
|
|
287
|
+
<summary>Using scroll position restoration feature without <code>NavigationStack</code></summary>
|
|
288
|
+
|
|
289
|
+
######
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
import { WebBrowserSession } from 'navigation-stack'
|
|
295
|
+
import { ScrollPositionRestoration } from 'navigation-stack/scroll-position'
|
|
296
|
+
|
|
297
|
+
// Create a `ScrollPositionRestoration`.
|
|
298
|
+
const scrollPositionRestoration = new ScrollPositionRestoration(new WebBrowserSession())
|
|
299
|
+
|
|
300
|
+
//----------------------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
// If you decide to use `NavigationStack` or Redux-way `createMiddlewares()` for navigation,
|
|
303
|
+
// it should be tied to the same session.
|
|
304
|
+
//
|
|
305
|
+
// const navigationStack = new NavigationStack(session)
|
|
306
|
+
// navigationStack.init()
|
|
307
|
+
//
|
|
308
|
+
// Or, navigation could be performed by any other means such as using `window.history.pushState()`.
|
|
309
|
+
// The only requirement is for the "current location" object to have a `key` property
|
|
310
|
+
// which the standard `window.location` object doesn't provide.
|
|
311
|
+
//
|
|
312
|
+
window.history.replaceState({ key: '123' }, '', '/initial-location')
|
|
313
|
+
|
|
314
|
+
// Render the initial location.
|
|
315
|
+
document.body.innerHTML = '<div> Initial Location </div>'
|
|
316
|
+
|
|
317
|
+
// Immediately after a page has been rendered, without any delay,
|
|
318
|
+
// call `.locationRendered()` method with the "current location" object as the argument.
|
|
319
|
+
scrollPositionRestoration.locationRendered({ key: '123', pathname: '/initial-location' })
|
|
320
|
+
|
|
321
|
+
//----------------------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
// Navigate to a new location.
|
|
324
|
+
//
|
|
325
|
+
// For example, it could use `NavigationStack` for navigation.
|
|
326
|
+
//
|
|
327
|
+
// navigationStack.push('/new-location')
|
|
328
|
+
//
|
|
329
|
+
// Or, navigation could be performed by any other means such as using `window.history.pushState()`.
|
|
330
|
+
//
|
|
331
|
+
window.history.pushState({ key: '456' }, '', '/new-location')
|
|
332
|
+
|
|
333
|
+
// Render the new location.
|
|
334
|
+
document.body.innerHTML = '<div> New Location </div>'
|
|
335
|
+
|
|
336
|
+
// The new location is now rendered.
|
|
337
|
+
// Call `.locationRendered()` immediately after it has been rendered, i.e. without any delay.
|
|
338
|
+
// There's no scroll position to restore because it's not a previously-visited location.
|
|
339
|
+
// The "current location" object must have a `key`.
|
|
340
|
+
scrollPositionRestoration.locationRendered({ key: '456', pathname: '/new-location' })
|
|
341
|
+
|
|
342
|
+
//----------------------------------------------------------------------------------------
|
|
343
|
+
|
|
344
|
+
// Navigate "back" to the initial location.
|
|
345
|
+
//
|
|
346
|
+
// For example, it could use `NavigationStack` for navigation.
|
|
347
|
+
//
|
|
348
|
+
// navigationStack.shift(-1)
|
|
349
|
+
//
|
|
350
|
+
// Or, navigation could be performed by any other means such as using `window.history.go()`.
|
|
351
|
+
//
|
|
352
|
+
window.history.go(-1)
|
|
353
|
+
|
|
354
|
+
// Render the initial location.
|
|
355
|
+
document.body.innerHTML = '<div> Initial Location </div>'
|
|
356
|
+
|
|
357
|
+
// The initial location is now rendered.
|
|
358
|
+
// Call `.locationRendered()` immediately after it has been rendered, i.e. without any delay.
|
|
359
|
+
// It will restore the scroll position at the initial location.
|
|
360
|
+
// The "current location" object must have a `key`.
|
|
361
|
+
scrollPositionRestoration.locationRendered({ key: '123', pathname: '/initial-location' })
|
|
362
|
+
|
|
363
|
+
//----------------------------------------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
// (optional)
|
|
366
|
+
// When the user is about to close the application,
|
|
367
|
+
// stop the `ScrollPositionRestoration` and clean up any of its listeners.
|
|
368
|
+
// This is not required in a web browser because it cleans up all listeners
|
|
369
|
+
// automatically when closing a tab.
|
|
370
|
+
scrollPositionRestoration.stop()
|
|
371
|
+
|
|
372
|
+
// (optional)
|
|
373
|
+
// In case of using `NavigationStack` for navigation,
|
|
374
|
+
// stop the `NavigationStack` and clean up any of its listeners.
|
|
375
|
+
//
|
|
376
|
+
// navigationStack.stop()
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
`ScrollPositionRestoration` provides methods:
|
|
380
|
+
|
|
381
|
+
* `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.
|
|
382
|
+
* `locationRendered(location)` — Call it every time a different location has been rendered, including the initial location, without any delay, i.e. immediately after a different location has been rendered. The location argument must have a `key`.
|
|
383
|
+
* `stop()` — Stops scroll position restoration and clears any listeners or timers.
|
|
384
|
+
</details>
|
|
168
385
|
|
|
169
386
|
## Base Path
|
|
170
387
|
|
|
388
|
+
<!--
|
|
171
389
|
If the web application is hosted under a certain URL prefix, it should be specified in `createMiddlewares()` call as `basePath` parameter.
|
|
172
390
|
|
|
173
391
|
```js
|
|
174
|
-
createMiddlewares(session, { basePath?: '/base
|
|
392
|
+
createMiddlewares(session, { basePath?: '/base-path' })
|
|
175
393
|
```
|
|
394
|
+
-->
|
|
176
395
|
|
|
177
|
-
|
|
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.
|
|
396
|
+
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
397
|
|
|
181
398
|
```js
|
|
182
|
-
|
|
399
|
+
new NavigationStack(new WebBrowserSession(), { basePath: '/base-path' })
|
|
400
|
+
```
|
|
183
401
|
|
|
184
|
-
|
|
402
|
+
## Session
|
|
185
403
|
|
|
186
|
-
|
|
404
|
+
A "session" ties `NavigationStack` to the environment it operates in, such as a web browser.
|
|
187
405
|
|
|
188
|
-
|
|
406
|
+
Three different "session" implementations are shipped with this package:
|
|
189
407
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
408
|
+
- Use `WebBrowserSession` in a web browser. Such session survives a page refresh and is automatically destroyed when the web browser tab gets closed.
|
|
409
|
+
- 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.
|
|
410
|
+
- 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.
|
|
411
|
+
|
|
412
|
+
<details>
|
|
413
|
+
<summary>See <code>ServerSideRenderSession</code> example</summary>
|
|
414
|
+
|
|
415
|
+
######
|
|
416
|
+
|
|
417
|
+
```js
|
|
418
|
+
const navigationStack = new NavigationStack(new ServerSideRenderSession())
|
|
419
|
+
|
|
420
|
+
navigationStack.subscribe((location) => {
|
|
421
|
+
console.log('Current location', location)
|
|
422
|
+
})
|
|
193
423
|
|
|
194
|
-
|
|
424
|
+
// Sets the initial location.
|
|
425
|
+
// Triggers the subscription listener.
|
|
426
|
+
navigationStack.init('/initial-location')
|
|
427
|
+
|
|
428
|
+
// Navigates to a new location.
|
|
429
|
+
// Throws `ServerSideNavigationError` with a `location` property.
|
|
430
|
+
navigationStack.push('/new-location')
|
|
431
|
+
```
|
|
432
|
+
</details>
|
|
195
433
|
|
|
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
434
|
|
|
198
|
-
|
|
435
|
+
<details>
|
|
436
|
+
<summary>See <code>InMemorySession</code> example</summary>
|
|
199
437
|
|
|
200
|
-
|
|
438
|
+
######
|
|
201
439
|
|
|
202
440
|
```js
|
|
203
|
-
|
|
441
|
+
const navigationStack = new NavigationStack(new InMemorySession())
|
|
442
|
+
|
|
443
|
+
navigationStack.subscribe((location) => {
|
|
444
|
+
console.log('Current location', location)
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
// Sets the initial location.
|
|
448
|
+
// Triggers the subscription listener.
|
|
449
|
+
navigationStack.init('/initial-location')
|
|
450
|
+
|
|
451
|
+
// Navigates to a new location.
|
|
452
|
+
// Triggers the subscription listener.
|
|
453
|
+
navigationStack.push('/new-location')
|
|
454
|
+
```
|
|
455
|
+
</details>
|
|
456
|
+
|
|
457
|
+
######
|
|
458
|
+
|
|
459
|
+
Every "session" has a unique `key`.
|
|
460
|
+
|
|
461
|
+
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.
|
|
462
|
+
|
|
463
|
+
However, if someone prefers to completely bypass `NavigationStack` and interact with a "session" object directly, they could do so.
|
|
464
|
+
|
|
465
|
+
<details>
|
|
466
|
+
<summary>See "session" API</summary>
|
|
467
|
+
|
|
468
|
+
######
|
|
469
|
+
|
|
470
|
+
* `key: string` — A unique ID of the session.
|
|
471
|
+
* `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:
|
|
472
|
+
* `operation: string` — The type of navigation that led to the location.
|
|
473
|
+
* `INIT` in case of the initial location before any navigation has taken place.
|
|
474
|
+
* `SHIFT` when the user performs a "Back" or "Forward" navigation, or after a `.shift()` navigation which is essentially a "back or forward navigation".
|
|
475
|
+
* `PUSH` in case of a `.push()` navigation, i.e. "normal navigation via a hyperlink".
|
|
476
|
+
* `REPLACE` in case of a `.replace()` navigation, i.e. "redirect".
|
|
477
|
+
* `delta: number` — the difference between the `index` of the current location and the `index` of the previous location.
|
|
478
|
+
* `0` for the initial location before any navigation has taken place.
|
|
479
|
+
* `1` after a `.push()` navigation, i.e. "normal navigation via a hyperlink".
|
|
480
|
+
* `0` after a `.replace()` navigation, i.e. "redirect".
|
|
481
|
+
* `delta: number` after a `.shift(delta)` navigation, i.e. "back or forward navigation".
|
|
482
|
+
* `-1` after the user clicks a "Back" button in their web browser.
|
|
483
|
+
* `1` after the user clicks a "Forward" button in their web browser.
|
|
484
|
+
<!-- * `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. -->
|
|
485
|
+
* `start(initialLocation?: object)` — Starts the session. The `initialLocation` argument is optional when the session can read it from somewhere. For example, `WebBrowserSession` can read `initialLocation` from `window.location`.
|
|
486
|
+
* `stop()` — Stops the session. Cleans up any listeners, etc.
|
|
487
|
+
* `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.
|
|
488
|
+
* `shift(delta: number)` — Navigates "back" or "forward" by skipping a specified count of pages. Negative `delta` skips backwards, positive `delta` skips forward.
|
|
489
|
+
</details>
|
|
204
490
|
|
|
491
|
+
## Utility
|
|
492
|
+
|
|
493
|
+
This package exports a few utility functions for transforming locations.
|
|
494
|
+
|
|
495
|
+
```js
|
|
205
496
|
import {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
497
|
+
getLocationUrl,
|
|
498
|
+
parseLocationUrl,
|
|
499
|
+
parseInputLocation,
|
|
500
|
+
addBasePath,
|
|
501
|
+
removeBasePath
|
|
211
502
|
} from 'navigation-stack'
|
|
212
503
|
|
|
213
|
-
|
|
504
|
+
// The following two are "mutually inverse functions":
|
|
505
|
+
// one maps a `location` object to a URL string
|
|
506
|
+
// and the other maps a URL string to a `location` object.
|
|
214
507
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
locationReducer, // Reducer function. For example, `locationReducer()`.
|
|
218
|
-
applyMiddleware(...createMiddlewares(session))
|
|
219
|
-
)
|
|
508
|
+
// Converts a location object to a location URL.
|
|
509
|
+
getLocationUrl({ pathname: '/abc', search: '?d=e' }) === '/abc?d=e'
|
|
220
510
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}
|
|
227
|
-
|
|
511
|
+
// Parses a location URL to a location object.
|
|
512
|
+
// If there're no query parameters, `query` property will be an empty object.
|
|
513
|
+
parseLocationUrl('/abc?d=e') === {
|
|
514
|
+
pathname: '/abc',
|
|
515
|
+
search: '?d=e',
|
|
516
|
+
query: { d: 'e' },
|
|
517
|
+
hash: ''
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// The following function parses a non-strict location object to a strict one.
|
|
521
|
+
// It also parses a location URL to a location object.
|
|
522
|
+
|
|
523
|
+
parseInputLocation({ pathname: '/abc', search: '?d=e' }) === {
|
|
524
|
+
pathname: '/abc',
|
|
525
|
+
search: '?d=e',
|
|
526
|
+
query: { d: 'e' },
|
|
527
|
+
hash: ''
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
parseInputLocation('/abc?d=e') === {
|
|
531
|
+
pathname: '/abc',
|
|
532
|
+
search: '?d=e',
|
|
533
|
+
query: { d: 'e' },
|
|
534
|
+
hash: ''
|
|
535
|
+
}
|
|
228
536
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
store.dispatch(Actions.init())
|
|
537
|
+
// The following two functions can be used to add base path to a location
|
|
538
|
+
// or to remove it from it.
|
|
232
539
|
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
540
|
+
// Adds `basePath` to a location object or a location URL.
|
|
541
|
+
addBasePath('/abc', '/base-path') === '/base-path/abc'
|
|
542
|
+
addBasePath({ pathname: '/abc' }, '/base-path') === { pathname: '/base-path/abc' }
|
|
236
543
|
|
|
237
|
-
//
|
|
238
|
-
|
|
544
|
+
// Removes `basePath` from a location object or a location URL.
|
|
545
|
+
// If `basePath` is not present in location, it won't do anything.
|
|
546
|
+
removeBasePath('/base-path/abc', '/base-path') === '/abc';
|
|
547
|
+
removeBasePath({ pathname: '/base-path/abc' }, '/base-path') === { pathname: '/abc' }
|
|
239
548
|
```
|
|
240
549
|
|
|
241
550
|
## Block Navigation
|
|
242
551
|
|
|
243
|
-
`navigation-stack` provides the ability to block navigation. Call `addNavigationBlocker()`
|
|
552
|
+
`navigation-stack` provides the ability to block navigation. Call `addNavigationBlocker()` function to set up a "navigation blocker".
|
|
244
553
|
|
|
245
554
|
```js
|
|
246
|
-
import { createStore, applyMiddleware } from 'redux'
|
|
247
|
-
|
|
248
555
|
import {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
Actions,
|
|
252
|
-
BrowserSession,
|
|
556
|
+
NavigationStack,
|
|
557
|
+
WebBrowserSession,
|
|
253
558
|
addNavigationBlocker
|
|
254
559
|
} from 'navigation-stack'
|
|
255
560
|
|
|
256
|
-
|
|
561
|
+
// Create a session.
|
|
562
|
+
const session = new WebBrowserSession()
|
|
257
563
|
|
|
258
|
-
// Create a
|
|
259
|
-
const
|
|
260
|
-
locationReducer, // Reducer function. For example, `locationReducer()`.
|
|
261
|
-
applyMiddleware(...createMiddlewares(session))
|
|
262
|
-
)
|
|
263
|
-
|
|
264
|
-
// Initialize navigation.
|
|
265
|
-
store.dispatch(Actions.init())
|
|
564
|
+
// Create a `NavigationStack` instance.
|
|
565
|
+
const navigationStack = new NavigationStack(session)
|
|
266
566
|
|
|
267
|
-
// Add navigation blocker.
|
|
567
|
+
// Add a navigation blocker.
|
|
568
|
+
// It should be tied to the same "session".
|
|
268
569
|
const removeNavigationBlocker = addNavigationBlocker(
|
|
269
570
|
session,
|
|
270
571
|
(newLocation) => {
|
|
271
|
-
// Returning `true` means "
|
|
572
|
+
// Returning `true` means "this navigation should be blocked".
|
|
272
573
|
return true
|
|
273
574
|
}
|
|
274
575
|
);
|
|
275
576
|
|
|
276
|
-
//
|
|
277
|
-
|
|
577
|
+
// Because the navigation is blocked, current location will not change here.
|
|
578
|
+
//
|
|
579
|
+
// The URL in the web browser's address bar will stay the same
|
|
580
|
+
// and no new entries will be added in the web browser's navigation history.
|
|
581
|
+
//
|
|
582
|
+
navigationStack.push('/new-location')
|
|
278
583
|
|
|
279
584
|
// Remove the navigation blocker.
|
|
280
585
|
removeNavigationBlocker()
|
|
281
586
|
|
|
282
|
-
//
|
|
283
|
-
|
|
587
|
+
// With the blocker removed, current location will be set to a new one.
|
|
588
|
+
//
|
|
589
|
+
// This also updates the URL in the web browser's address bar
|
|
590
|
+
// and adds a new entry in the web browser's navigation history.
|
|
591
|
+
//
|
|
592
|
+
navigationStack.push('/new-location')
|
|
284
593
|
```
|
|
285
594
|
|
|
286
595
|
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
596
|
|
|
288
|
-
The `newLocation` argument of a blocker function
|
|
597
|
+
The `newLocation` argument of a blocker function might not necessarily have a `key` or `index` property but other properties are present.
|
|
289
598
|
|
|
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
|
|
599
|
+
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
600
|
|
|
292
|
-
##
|
|
601
|
+
## Data Storage
|
|
602
|
+
|
|
603
|
+
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.
|
|
604
|
+
|
|
605
|
+
Different types of data could be stored under a different `key`.
|
|
606
|
+
|
|
607
|
+
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
608
|
|
|
294
|
-
|
|
609
|
+
`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
610
|
|
|
296
611
|
```js
|
|
612
|
+
import { WebBrowserSession } from 'navigation-stack'
|
|
613
|
+
import { DataStorage, LocationDataStorage } from 'navigation-stack/data-storage'
|
|
614
|
+
|
|
615
|
+
const session = new WebBrowserSession()
|
|
616
|
+
|
|
617
|
+
// `DataStorage` example
|
|
618
|
+
|
|
619
|
+
const dataStorage = new DataStorage(session, { namespace: 'my-namespace' })
|
|
620
|
+
|
|
621
|
+
dataStorage.set('key', 123)
|
|
622
|
+
dataStorage.get('key') === 123
|
|
623
|
+
|
|
624
|
+
// `LocationDataStorage` example
|
|
625
|
+
|
|
626
|
+
const locationDataStorage = new LocationDataStorage(session, { namespace: 'my-namespace' })
|
|
627
|
+
|
|
628
|
+
const location = { pathname: '/abc' }
|
|
629
|
+
|
|
630
|
+
locationDataStorage.set(location, 'key', 123)
|
|
631
|
+
locationDataStorage.get(location, 'key') === 123
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
`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".
|
|
635
|
+
|
|
636
|
+
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.
|
|
637
|
+
|
|
638
|
+
## Redux
|
|
639
|
+
|
|
640
|
+
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.
|
|
641
|
+
|
|
642
|
+
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.
|
|
643
|
+
|
|
644
|
+
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)).
|
|
645
|
+
|
|
646
|
+
<details>
|
|
647
|
+
<summary>See <code>redux</code>-style API</summary>
|
|
648
|
+
|
|
649
|
+
######
|
|
650
|
+
|
|
651
|
+
Start by creating a Redux "store" with `navigation-stack` middlewares.
|
|
652
|
+
|
|
653
|
+
```js
|
|
654
|
+
import { createStore, applyMiddleware } from 'redux';
|
|
655
|
+
|
|
297
656
|
import {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
} from 'navigation-stack'
|
|
657
|
+
createMiddlewares,
|
|
658
|
+
locationReducer,
|
|
659
|
+
Actions,
|
|
660
|
+
WebBrowserSession,
|
|
661
|
+
} from 'navigation-stack';
|
|
303
662
|
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
663
|
+
// Create a Redux store.
|
|
664
|
+
const store = createStore(
|
|
665
|
+
// Reducer function. For example, `locationReducer()`.
|
|
666
|
+
locationReducer,
|
|
667
|
+
// It should be tied to a navigation "session".
|
|
668
|
+
applyMiddleware(...createMiddlewares(new WebBrowserSession())),
|
|
669
|
+
);
|
|
670
|
+
```
|
|
312
671
|
|
|
313
|
-
|
|
314
|
-
getLocationUrl({ pathname: '/abc', search: '?d=e', hash: '' }) === '/abc?d=e'
|
|
672
|
+
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
673
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
674
|
+
```js
|
|
675
|
+
// Sets the initial `location`.
|
|
676
|
+
//
|
|
677
|
+
// Accepts either a relative URL string or a location object.
|
|
678
|
+
//
|
|
679
|
+
// The initial location argument could be omitted for `WebBrowserSession`
|
|
680
|
+
// because it can read it by itself from `window.location`.
|
|
681
|
+
// Other types of session such as `InMemorySession` or `ServerSideRenderSession`
|
|
682
|
+
// don't have an initial location and require the initial location argument
|
|
683
|
+
// to be specified explicitly when creating an `Actions.init(initialLocation)` action.
|
|
684
|
+
//
|
|
685
|
+
store.dispatch(Actions.init());
|
|
686
|
+
```
|
|
319
687
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
688
|
+
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.
|
|
689
|
+
|
|
690
|
+
```js
|
|
691
|
+
let currentLocation;
|
|
692
|
+
|
|
693
|
+
// Create a Redux store.
|
|
694
|
+
const store = createStore(
|
|
695
|
+
locationReducer, // Reducer function. For example, `locationReducer()`.
|
|
696
|
+
applyMiddleware(...createMiddlewares(new WebBrowserSession())),
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// Subscribe to any potential Redux state changes.
|
|
700
|
+
const unsubscribe = store.subscribe(() => {
|
|
701
|
+
const previousLocation = currentLocation;
|
|
702
|
+
currentLocation = store.getState(); // In case of using `locationReducer()`.
|
|
703
|
+
if (currentLocation !== previousLocation) {
|
|
704
|
+
console.log('Current location', currentLocation);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
Now ready to perform navigation actions by dispatching any of the available `Actions`.
|
|
710
|
+
|
|
711
|
+
```js
|
|
712
|
+
// Sets the `location` to be a new location.
|
|
713
|
+
//
|
|
714
|
+
// Also updates the URL in the web browser's address bar.
|
|
715
|
+
//
|
|
716
|
+
// Also adds a new entry in the web browser's navigation history.
|
|
717
|
+
//
|
|
718
|
+
store.dispatch(Actions.push('/new-location'));
|
|
719
|
+
|
|
720
|
+
// Sets the `location` to be a new location.
|
|
721
|
+
//
|
|
722
|
+
// Also updates the URL in the web browser's address bar.
|
|
723
|
+
//
|
|
724
|
+
// Does not add a new entry in the web browser's navigation history
|
|
725
|
+
// which is the only difference between this and `Actions.push()`.
|
|
726
|
+
//
|
|
727
|
+
store.dispatch(Actions.replace('/new-location'));
|
|
728
|
+
|
|
729
|
+
// Sets the `location` to be a previous one (if there is one).
|
|
730
|
+
// One could think of it as an equivalent of clicking a "Back" button in a web browser.
|
|
731
|
+
//
|
|
732
|
+
// Also updates the URL in the web browser's address bar.
|
|
733
|
+
//
|
|
734
|
+
// Also shifts the current position in the web browser's navigation history.
|
|
735
|
+
//
|
|
736
|
+
store.dispatch(Actions.shift(-1));
|
|
737
|
+
|
|
738
|
+
// Sets the `location` to be a next one (if there is one).
|
|
739
|
+
// One could think of it as an equivalent of clicking a "Forward" button in a web browser.
|
|
740
|
+
//
|
|
741
|
+
// Also updates the URL in the web browser's address bar.
|
|
742
|
+
//
|
|
743
|
+
// Also shifts the current position in the web browser's navigation history.
|
|
744
|
+
//
|
|
745
|
+
store.dispatch(Actions.shift(1));
|
|
324
746
|
```
|
|
325
747
|
|
|
748
|
+
To get the current location:
|
|
749
|
+
|
|
750
|
+
```js
|
|
751
|
+
// When `locationReducer()` is used, `store.getState()` returns the current location.
|
|
752
|
+
const location = store.getState();
|
|
753
|
+
console.log(location);
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
(optional) After the user is done using the app, stop the session and clean up any listeners.
|
|
757
|
+
|
|
758
|
+
```js
|
|
759
|
+
// (optional)
|
|
760
|
+
// When the user closes the application,
|
|
761
|
+
// stop the session and clean up any listeners.
|
|
762
|
+
// There's no need to do this in a web browser.
|
|
763
|
+
unsubscribe();
|
|
764
|
+
store.dispatch(Actions.stop());
|
|
765
|
+
```
|
|
766
|
+
</details>
|
|
767
|
+
|
|
326
768
|
## Development
|
|
327
769
|
|
|
328
770
|
Clone the repository. Then:
|
|
@@ -332,3 +774,9 @@ yarn
|
|
|
332
774
|
yarn format
|
|
333
775
|
yarn test
|
|
334
776
|
```
|
|
777
|
+
|
|
778
|
+
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.
|
|
779
|
+
|
|
780
|
+
## GitHub
|
|
781
|
+
|
|
782
|
+
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.
|