navigation-stack 0.1.3 → 0.2.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 (77) hide show
  1. package/.github/workflows/main.yml +39 -39
  2. package/README.md +128 -27
  3. package/lib/cjs/{LocationStateStorage.js → LocationDataStorage.js} +5 -5
  4. package/lib/cjs/addBeforeLocationChangeListener.js +7 -0
  5. package/lib/cjs/beforeLocationChangeListeners.js +51 -0
  6. package/lib/cjs/createMiddlewares.js +21 -17
  7. package/lib/cjs/index.js +9 -9
  8. package/lib/cjs/middleware/createBasePathMiddleware.js +2 -2
  9. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +39 -0
  10. package/lib/cjs/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
  11. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +62 -29
  12. package/lib/cjs/middleware/createTransformLocationMiddleware.js +2 -2
  13. package/lib/cjs/navigationBlockers.js +55 -47
  14. package/lib/cjs/normalizeInputLocation.js +1 -0
  15. package/lib/cjs/parseLocationUrl.js +2 -0
  16. package/lib/cjs/parseQueryFromSearch.js +1 -1
  17. package/lib/cjs/session/BrowserSession.js +229 -0
  18. package/lib/cjs/session/MemorySession.js +223 -0
  19. package/lib/cjs/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -16
  20. package/lib/esm/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
  21. package/lib/esm/addBeforeLocationChangeListener.js +2 -0
  22. package/lib/esm/beforeLocationChangeListeners.js +44 -0
  23. package/lib/esm/createMiddlewares.js +21 -17
  24. package/lib/esm/index.js +4 -4
  25. package/lib/esm/middleware/createBasePathMiddleware.js +2 -2
  26. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +34 -0
  27. package/lib/esm/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +11 -13
  28. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +62 -29
  29. package/lib/esm/middleware/createTransformLocationMiddleware.js +2 -2
  30. package/lib/esm/navigationBlockers.js +55 -47
  31. package/lib/esm/normalizeInputLocation.js +1 -0
  32. package/lib/esm/parseLocationUrl.js +2 -0
  33. package/lib/esm/parseQueryFromSearch.js +1 -1
  34. package/lib/esm/session/BrowserSession.js +223 -0
  35. package/lib/esm/session/MemorySession.js +217 -0
  36. package/lib/esm/{environment/ServerEnvironment.js → session/ServerSession.js} +27 -15
  37. package/lib/index.d.ts +64 -59
  38. package/package.json +4 -4
  39. package/src/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
  40. package/src/addBeforeLocationChangeListener.js +2 -0
  41. package/src/beforeLocationChangeListeners.js +54 -0
  42. package/src/createMiddlewares.js +21 -17
  43. package/src/index.js +4 -4
  44. package/src/middleware/createBasePathMiddleware.js +2 -2
  45. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +40 -0
  46. package/src/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
  47. package/src/middleware/createNavigationBlockerMiddleware.js +68 -28
  48. package/src/middleware/createTransformLocationMiddleware.js +2 -2
  49. package/src/navigationBlockers.js +68 -49
  50. package/src/normalizeInputLocation.js +1 -0
  51. package/src/parseLocationUrl.js +2 -0
  52. package/src/parseQueryFromSearch.js +1 -1
  53. package/src/session/BrowserSession.js +225 -0
  54. package/src/session/MemorySession.js +219 -0
  55. package/src/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -15
  56. package/test/{LocationStateStorage.test.js → LocationDataStorage.test.js} +6 -6
  57. package/test/createMiddlewares.test.js +2 -2
  58. package/test/helpers.js +1 -1
  59. package/test/index.test.js +3 -3
  60. package/test/middleware/createBasePathMiddleware.test.js +7 -7
  61. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +141 -0
  62. package/test/middleware/createNavigationBlockerMiddleware.test.js +96 -97
  63. package/test/middleware/createTransformLocationMiddleware.test.js +1 -1
  64. package/test/normalizeInputLocation.test.js +3 -0
  65. package/test/parseLocationUrl.test.js +2 -0
  66. package/test/{environment/BrowserEnvironment.test.js → session/BrowserSession.test.js} +35 -18
  67. package/test/session/MemorySession.test.js +244 -0
  68. package/test/session/ServerSession.test.js +23 -0
  69. package/types/index.d.ts +64 -59
  70. package/lib/cjs/environment/BrowserEnvironment.js +0 -111
  71. package/lib/cjs/environment/MemoryEnvironment.js +0 -150
  72. package/lib/esm/environment/BrowserEnvironment.js +0 -104
  73. package/lib/esm/environment/MemoryEnvironment.js +0 -143
  74. package/src/environment/BrowserEnvironment.js +0 -109
  75. package/src/environment/MemoryEnvironment.js +0 -151
  76. package/test/environment/MemoryEnvironment.test.js +0 -218
  77. package/test/environment/ServerEnvironment.test.js +0 -23
@@ -1,39 +1,39 @@
1
- name: Build and test
2
- on:
3
- push:
4
- branches: [master]
5
- pull_request:
6
- branches: [master]
7
-
8
- env:
9
- DISPLAY: :99.0
10
-
11
- jobs:
12
- build:
13
- runs-on: ubuntu-latest
14
- strategy:
15
- matrix:
16
- browser: ['ChromeCi', 'Firefox']
17
- node-version: [16.x]
18
-
19
- steps:
20
- - uses: actions/checkout@v2
21
- - name: Use Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v1
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
-
26
- - name: Setup firefox
27
- if: ${{ matrix.browser == 'Firefox' }}
28
- uses: browser-actions/setup-firefox@latest
29
- with:
30
- firefox-version: 'latest'
31
- - name: Setup xvfb
32
- run: |
33
- sudo apt-get install xvfb
34
- Xvfb $DISPLAY -screen 0 1024x768x24 > /dev/null 2>&1 &
35
- - run: yarn install --frozen-lockfile
36
- - env:
37
- BROWSER: ${{ matrix.browser }}
38
- run: yarn test --coverage
39
- - run: node_modules/.bin/codecov
1
+ name: Build and test
2
+ on:
3
+ push:
4
+ branches: [master]
5
+ pull_request:
6
+ branches: [master]
7
+
8
+ env:
9
+ DISPLAY: :99.0
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ strategy:
15
+ matrix:
16
+ browser: ['ChromeCi', 'Firefox']
17
+ node-version: [16.x]
18
+
19
+ steps:
20
+ - uses: actions/checkout@v2
21
+ - name: Use Node.js ${{ matrix.node-version }}
22
+ uses: actions/setup-node@v1
23
+ with:
24
+ node-version: ${{ matrix.node-version }}
25
+
26
+ - name: Setup firefox
27
+ if: ${{ matrix.browser == 'Firefox' }}
28
+ uses: browser-actions/setup-firefox@latest
29
+ with:
30
+ firefox-version: 'latest'
31
+ - name: Setup xvfb
32
+ run: |
33
+ sudo apt-get install xvfb
34
+ Xvfb $DISPLAY -screen 0 1024x768x24 > /dev/null 2>&1 &
35
+ - run: yarn install --frozen-lockfile
36
+ - env:
37
+ BROWSER: ${{ matrix.browser }}
38
+ run: yarn test --coverage
39
+ - run: node_modules/.bin/codecov
package/README.md CHANGED
@@ -24,14 +24,16 @@ import {
24
24
  createMiddlewares,
25
25
  locationReducer,
26
26
  Actions,
27
- BrowserEnvironment
27
+ BrowserSession
28
28
  } from 'navigation-stack'
29
29
 
30
+ // Create a Redux store.
30
31
  const store = createStore(
31
32
  locationReducer, // Reducer function. For example, `locationReducer()`.
32
- applyMiddleware(...createMiddlewares(new BrowserEnvironment()))
33
+ applyMiddleware(...createMiddlewares(new BrowserSession()))
33
34
  )
34
35
 
36
+ // Initialize navigation.
35
37
  store.dispatch(Actions.init())
36
38
  ```
37
39
 
@@ -88,53 +90,100 @@ With this reducer, `store.getState()` will return the current location.
88
90
 
89
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.
90
92
 
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:
94
+ * `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
+ * `POP` 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
111
+
91
112
  One could use Redux'es standard [subscription mechanisms](https://redux.js.org/api/store#subscribelistener) to immediately get notified of current location changes.
92
113
 
114
+ ```js
115
+ let currentLocation
116
+
117
+ // Create a Redux store.
118
+ const store = createStore(
119
+ locationReducer, // Reducer function. For example, `locationReducer()`.
120
+ applyMiddleware(...createMiddlewares(new BrowserSession()))
121
+ )
122
+
123
+ // Subscribe to any potential Redux state changes.
124
+ const unsubscribe = store.subscribe(() => {
125
+ const previousLocation = currentLocation
126
+ currentLocation = store.getState() // In case of using `locationReducer()`.
127
+ if (currentLocation !== previousLocation) {
128
+ console.log('Location has changed')
129
+ }
130
+ })
131
+
132
+ // Initialize navigation.
133
+ // Emitting a Redux action will trigger the listener.
134
+ store.dispatch(Actions.init())
135
+
136
+ // Stop listening to current location changes.
137
+ unsubscribe()
138
+ ```
139
+
93
140
  ## Why Redux?
94
141
 
95
142
  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.
96
143
 
97
144
  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.
98
145
 
99
- ## Environment
146
+ ## Session
147
+
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`.
100
149
 
101
150
  ```js
102
151
  import {
103
- BrowserEnvironment,
104
- ServerEnvironment,
105
- MemoryEnvironment
152
+ BrowserSession,
153
+ ServerSession,
154
+ MemorySession
106
155
  } from 'navigation-stack'
107
156
 
108
- new BrowserEnvironment()
109
- new ServerEnvironment('/location-url')
110
- new MemoryEnvironment('/location-url')
157
+ new BrowserSession()
158
+ new ServerSession('/initial-location-url')
159
+ new MemorySession('/initial-location-url')
111
160
  ```
112
161
 
113
- - Use `BrowserEnvironment` in a web browser.
114
- - Use `ServerEnvironment` in server-side rendering.
115
- - Use `MemoryEnvironment` in tests.
116
- - `MemoryEnvironment` supports an optional second argument — an `options` object with properties:
117
- - `save(state)` — Saves the environment state.
118
- - `load()` — Loads a previously-saved environment state.
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`.
119
168
 
120
169
  ## Base Path
121
170
 
122
171
  If the web application is hosted under a certain URL prefix, it should be specified in `createMiddlewares()` call as `basePath` parameter.
123
172
 
124
173
  ```js
125
- createMiddlewares(environment, { basePath?: '/base/path' })
174
+ createMiddlewares(session, { basePath?: '/base/path' })
126
175
  ```
127
176
 
128
177
  ## Location State Storage
129
178
 
130
- One could use an environment-specific `LocationStateStorage` in order to store location-specific state. 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.
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.
131
180
 
132
181
  ```js
133
- import { BrowserEnvironment, LocationStateStorage } from 'navigation-stack'
182
+ import { BrowserSession, LocationDataStorage } from 'navigation-stack'
134
183
 
135
- const environment = new BrowserEnvironment()
184
+ const session = new BrowserSession()
136
185
 
137
- const storage = new LocationStateStorage(environment, { namespace?: 'optional-namespace' })
186
+ const storage = new LocationDataStorage(session, { namespace?: 'optional-namespace' })
138
187
 
139
188
  const location = { pathname: '/abc' }
140
189
 
@@ -142,10 +191,57 @@ storage.set(location, 'key', 123)
142
191
  storage.get(location, 'key') === 123
143
192
  ```
144
193
 
145
- `LocationStateStorage` 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 state, that type of data is inherently non-essential and rather "nice-to-have".
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".
195
+
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
+
198
+ ## Get Notified Before Location Changes
199
+
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.
201
+
202
+ ```js
203
+ import { createStore, applyMiddleware } from 'redux'
204
+
205
+ import {
206
+ createMiddlewares,
207
+ locationReducer,
208
+ Actions,
209
+ BrowserSession,
210
+ addBeforeLocationChangeListener
211
+ } from 'navigation-stack'
212
+
213
+ const session = new BrowserSession()
214
+
215
+ // Create a Redux store.
216
+ const store = createStore(
217
+ locationReducer, // Reducer function. For example, `locationReducer()`.
218
+ applyMiddleware(...createMiddlewares(session))
219
+ )
220
+
221
+ // Subscribe to "before location change" events.
222
+ const removeBeforeLocationChangeListener = addBeforeLocationChangeListener(
223
+ session,
224
+ (newLocation) => {
225
+ console.log(newLocation)
226
+ }
227
+ );
228
+
229
+ // Initialize navigation.
230
+ // This will not trigger the listener because it's not a navigation.
231
+ store.dispatch(Actions.init())
232
+
233
+ // This navigation event will trigger the listener.
234
+ // `newLocation.action` will be "PUSH".
235
+ store.dispatch(Actions.push('/new/location'))
236
+
237
+ // Unsubscribe from "before location change" events.
238
+ removeBeforeLocationChangeListener()
239
+ ```
146
240
 
147
241
  ## Block Navigation
148
242
 
243
+ `navigation-stack` provides the ability to block navigation. Call `addNavigationBlocker()` exported function to set up a navigation blocker.
244
+
149
245
  ```js
150
246
  import { createStore, applyMiddleware } from 'redux'
151
247
 
@@ -153,21 +249,24 @@ import {
153
249
  createMiddlewares,
154
250
  locationReducer,
155
251
  Actions,
156
- BrowserEnvironment,
252
+ BrowserSession,
157
253
  addNavigationBlocker
158
254
  } from 'navigation-stack'
159
255
 
160
- const environment = new BrowserEnvironment()
256
+ const session = new BrowserSession()
161
257
 
258
+ // Create a Redux store.
162
259
  const store = createStore(
163
260
  locationReducer, // Reducer function. For example, `locationReducer()`.
164
- applyMiddleware(...createMiddlewares(environment))
261
+ applyMiddleware(...createMiddlewares(session))
165
262
  )
166
263
 
264
+ // Initialize navigation.
167
265
  store.dispatch(Actions.init())
168
266
 
267
+ // Add navigation blocker.
169
268
  const removeNavigationBlocker = addNavigationBlocker(
170
- environment,
269
+ session,
171
270
  (newLocation) => {
172
271
  // Returning `true` means "block this navigation".
173
272
  return true
@@ -177,7 +276,7 @@ const removeNavigationBlocker = addNavigationBlocker(
177
276
  // This navigation won't be performed.
178
277
  store.dispatch(Actions.push('/new/location'))
179
278
 
180
- // Disable the navigation blocker.
279
+ // Remove the navigation blocker.
181
280
  removeNavigationBlocker()
182
281
 
183
282
  // This navigation now will be performed.
@@ -186,6 +285,8 @@ store.dispatch(Actions.push('/new/location'))
186
285
 
187
286
  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`).
188
287
 
288
+ The `newLocation` argument of a blocker function won't necessarily have a `key` or `index` property but other properties are present.
289
+
189
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.
190
291
 
191
292
  ## Utility
@@ -201,7 +302,7 @@ import {
201
302
  } from 'navigation-stack'
202
303
 
203
304
  // Parses a location URL to a location object.
204
- // If there're no query parameters, `query` property will not be added.
305
+ // If there're no query parameters, `query` property will be an empty object.
205
306
  parseLocationUrl('/abc?d=e') === {
206
307
  pathname: '/abc',
207
308
  search: '?d=e',
@@ -4,7 +4,7 @@ exports.__esModule = true;
4
4
  exports.default = void 0;
5
5
  var _getLocationUrl = _interopRequireDefault(require("./getLocationUrl"));
6
6
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
- class LocationStateStorage {
7
+ class LocationDataStorage {
8
8
  constructor(environment, {
9
9
  namespace
10
10
  } = {}) {
@@ -15,7 +15,7 @@ class LocationStateStorage {
15
15
  get(location, key) {
16
16
  const stateKey = this._getStateKey(location, key);
17
17
  try {
18
- const value = this._environment.getState(stateKey);
18
+ const value = this._environment.dataStorage.get(stateKey);
19
19
  // === null is probably sufficient.
20
20
  if (value === null) {
21
21
  return undefined;
@@ -33,7 +33,7 @@ class LocationStateStorage {
33
33
  const stateKey = this._getStateKey(location, key);
34
34
  if (value === undefined) {
35
35
  try {
36
- this._environment.removeState(stateKey);
36
+ this._environment.dataStorage.remove(stateKey);
37
37
  } catch (error) {
38
38
  // No need to handle errors here.
39
39
  }
@@ -44,7 +44,7 @@ class LocationStateStorage {
44
44
  // value here is provided by the caller of this method.
45
45
  const valueString = JSON.stringify(value);
46
46
  try {
47
- this._environment.setState(stateKey, valueString);
47
+ this._environment.dataStorage.set(stateKey, valueString);
48
48
  } catch (error) {
49
49
  // No need to handle errors here either. If it didn't work, it didn't
50
50
  // work. We make no guarantees about actually saving the value.
@@ -56,5 +56,5 @@ class LocationStateStorage {
56
56
  return `${keyPrefix}|${key}`;
57
57
  }
58
58
  }
59
- exports.default = LocationStateStorage;
59
+ exports.default = LocationDataStorage;
60
60
  module.exports = exports.default;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = void 0;
5
+ var _beforeLocationChangeListeners = require("./beforeLocationChangeListeners");
6
+ exports.default = _beforeLocationChangeListeners.addBeforeLocationChangeListener;
7
+ module.exports = exports.default;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.addBeforeLocationChangeListener = addBeforeLocationChangeListener;
5
+ exports.getBeforeLocationChangeListeners = getBeforeLocationChangeListeners;
6
+ exports.removeAllBeforeLocationChangeListeners = removeAllBeforeLocationChangeListeners;
7
+ exports.runBeforeLocationChangeListeners = runBeforeLocationChangeListeners;
8
+ /* eslint-disable no-underscore-dangle */
9
+
10
+ function getBeforeLocationChangeListeners(session) {
11
+ return session._beforeLocationChangeListenersList || [];
12
+ }
13
+ function addBeforeLocationChangeListenerToTheList(listener, session) {
14
+ if (!session._beforeLocationChangeListenersList) {
15
+ session._beforeLocationChangeListenersList = [];
16
+ }
17
+ session._beforeLocationChangeListenersList.push(listener);
18
+ }
19
+ function removeBeforeLocationChangeListenerFromTheList(listener, session) {
20
+ if (session._beforeLocationChangeListenersList) {
21
+ session._beforeLocationChangeListenersList = session._beforeLocationChangeListenersList.filter(_ => _ !== listener);
22
+ }
23
+ }
24
+ function removeAllBeforeLocationChangeListeners(session) {
25
+ session._beforeLocationChangeListenersList = [];
26
+ }
27
+
28
+ // Runs the `listener` while ignoring any errors that might be thrown by it.
29
+ function runBeforeLocationChangeListener(listener, location) {
30
+ try {
31
+ listener(location);
32
+ } catch (error) {
33
+ // eslint-disable-next-line no-console
34
+ console.warn(`Ignoring before location change listener \`${listener.name}\` that failed with \`${error}\`.`);
35
+ // eslint-disable-next-line no-console
36
+ console.error(error);
37
+ }
38
+ }
39
+
40
+ // Runs all listeners in order.
41
+ function runBeforeLocationChangeListeners(navigationListeners, toLocation) {
42
+ for (const listener of navigationListeners) {
43
+ runBeforeLocationChangeListener(listener, toLocation);
44
+ }
45
+ }
46
+ function addBeforeLocationChangeListener(session, listener) {
47
+ addBeforeLocationChangeListenerToTheList(listener, session);
48
+ return () => {
49
+ removeBeforeLocationChangeListenerFromTheList(listener, session);
50
+ };
51
+ }
@@ -3,18 +3,19 @@
3
3
  exports.__esModule = true;
4
4
  exports.default = createMiddlewares;
5
5
  var _createBasePathMiddleware = _interopRequireDefault(require("./middleware/createBasePathMiddleware"));
6
- var _createEnvironmentMiddleware = _interopRequireDefault(require("./middleware/createEnvironmentMiddleware"));
6
+ var _createBeforeLocationChangeListenerMiddleware = _interopRequireDefault(require("./middleware/createBeforeLocationChangeListenerMiddleware"));
7
+ var _createLocationMiddleware = _interopRequireDefault(require("./middleware/createLocationMiddleware"));
7
8
  var _createNavigationBlockerMiddleware = _interopRequireDefault(require("./middleware/createNavigationBlockerMiddleware"));
8
9
  var _navigationActionMiddleware = _interopRequireDefault(require("./middleware/navigationActionMiddleware"));
9
10
  var _normalizeInputLocationMiddleware = _interopRequireDefault(require("./middleware/normalizeInputLocationMiddleware"));
10
11
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
11
- function createMiddlewares(environment, options) {
12
- // Allows temporarily ignoring certain environment location updates.
13
- let shouldIgnoreEnvironmentLocationUpdates = false;
14
- const ignoreEnvironmentLocationUpdates = func => {
15
- shouldIgnoreEnvironmentLocationUpdates = true;
12
+ function createMiddlewares(session, options) {
13
+ // Allows temporarily ignoring location update events.
14
+ let shouldIgnoreLocationSubscriptionEvents = false;
15
+ const ignoreLocationSubscriptionEvents = func => {
16
+ shouldIgnoreLocationSubscriptionEvents = true;
16
17
  func();
17
- shouldIgnoreEnvironmentLocationUpdates = false;
18
+ shouldIgnoreLocationSubscriptionEvents = false;
18
19
  };
19
20
  return [
20
21
  // Validates that the action "payload" (input location) is a proper `NormalizedInputLocation`.
@@ -26,18 +27,21 @@ function createMiddlewares(environment, options) {
26
27
  (0, _createBasePathMiddleware.default)(options && options.basePath),
27
28
  // Allows blocking navigation.
28
29
  // Handles `NAVIGATE` actions dispatched by the application itself.
29
- (0, _createNavigationBlockerMiddleware.default)(environment, {
30
- ignoreEnvironmentLocationUpdates
30
+ (0, _createNavigationBlockerMiddleware.default)(session, {
31
+ ignoreLocationSubscriptionEvents
31
32
  }),
32
- // This "middleware" performs the actual navigation according to the `environment` being used.
33
- // For example, when `BrowserEnvironment` is used, it calls methods of the `history` object.
34
- (0, _createEnvironmentMiddleware.default)(environment, {
35
- shouldIgnoreEnvironmentLocationUpdates: () => shouldIgnoreEnvironmentLocationUpdates
33
+ // This "middleware" performs the actual navigation according to the `session` being used.
34
+ // For example, when `BrowserSession` is used, it calls methods of the `history` object.
35
+ (0, _createLocationMiddleware.default)(session, {
36
+ shouldIgnoreLocationSubscriptionEvents: () => shouldIgnoreLocationSubscriptionEvents
36
37
  }),
37
38
  // Allows blocking navigation.
38
- // Handles location `UPDATE` actions dispatched by the environment.
39
- (0, _createNavigationBlockerMiddleware.default)(environment, {
40
- ignoreEnvironmentLocationUpdates
41
- })];
39
+ // Handles location `UPDATE` actions dispatched in response to location update events.
40
+ (0, _createNavigationBlockerMiddleware.default)(session, {
41
+ ignoreLocationSubscriptionEvents
42
+ }),
43
+ // Allows subscribing to upcoming location changes
44
+ // before those changes are applied in the `location` object in the state.
45
+ (0, _createBeforeLocationChangeListenerMiddleware.default)(session)];
42
46
  }
43
47
  module.exports = exports.default;
package/lib/cjs/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.removeBasePath = exports.parseLocationUrl = exports.locationReducer = exports.getLocationUrl = exports.createMiddlewares = exports.addNavigationBlocker = exports.addBasePath = exports.ServerEnvironment = exports.MemoryEnvironment = exports.LocationStateStorage = exports.BrowserEnvironment = exports.Actions = exports.ActionTypes = void 0;
4
+ exports.removeBasePath = exports.parseLocationUrl = exports.locationReducer = exports.getLocationUrl = exports.createMiddlewares = exports.addNavigationBlocker = exports.addBasePath = exports.ServerSession = exports.MemorySession = exports.LocationDataStorage = exports.BrowserSession = exports.Actions = exports.ActionTypes = void 0;
5
5
  var _Actions = _interopRequireDefault(require("./Actions"));
6
6
  exports.Actions = _Actions.default;
7
7
  var _ActionTypes = _interopRequireDefault(require("./ActionTypes"));
@@ -19,12 +19,12 @@ var _createMiddlewares = _interopRequireDefault(require("./createMiddlewares"));
19
19
  exports.createMiddlewares = _createMiddlewares.default;
20
20
  var _locationReducer = _interopRequireDefault(require("./locationReducer"));
21
21
  exports.locationReducer = _locationReducer.default;
22
- var _LocationStateStorage = _interopRequireDefault(require("./LocationStateStorage"));
23
- exports.LocationStateStorage = _LocationStateStorage.default;
24
- var _BrowserEnvironment = _interopRequireDefault(require("./environment/BrowserEnvironment"));
25
- exports.BrowserEnvironment = _BrowserEnvironment.default;
26
- var _MemoryEnvironment = _interopRequireDefault(require("./environment/MemoryEnvironment"));
27
- exports.MemoryEnvironment = _MemoryEnvironment.default;
28
- var _ServerEnvironment = _interopRequireDefault(require("./environment/ServerEnvironment"));
29
- exports.ServerEnvironment = _ServerEnvironment.default;
22
+ var _LocationDataStorage = _interopRequireDefault(require("./LocationDataStorage"));
23
+ exports.LocationDataStorage = _LocationDataStorage.default;
24
+ var _BrowserSession = _interopRequireDefault(require("./session/BrowserSession"));
25
+ exports.BrowserSession = _BrowserSession.default;
26
+ var _MemorySession = _interopRequireDefault(require("./session/MemorySession"));
27
+ exports.MemorySession = _MemorySession.default;
28
+ var _ServerSession = _interopRequireDefault(require("./session/ServerSession"));
29
+ exports.ServerSession = _ServerSession.default;
30
30
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
@@ -14,9 +14,9 @@ function createBasePathMiddleware(basePath) {
14
14
  transformInputLocation: location => {
15
15
  return (0, _basePath.addBasePath)(location, basePath);
16
16
  },
17
- // Transforms environment `Location` object:
17
+ // Transforms subscription `Location` object:
18
18
  // removes `basePath` from the URL.
19
- transformEnvironmentLocation: location => {
19
+ transformSubscriptionLocation: location => {
20
20
  return (0, _basePath.removeBasePath)(location, basePath);
21
21
  }
22
22
  });
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.default = createBeforeLocationChangeListenerMiddleware;
5
+ var _ActionTypes = _interopRequireDefault(require("../ActionTypes"));
6
+ var _beforeLocationChangeListeners = require("../beforeLocationChangeListeners");
7
+ function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
8
+ // Creates a "middleware" that calls upcoming navigation listeners.
9
+ function createBeforeLocationChangeListenerMiddleware(session) {
10
+ return function navigationListenerMiddleware() {
11
+ return next => action => {
12
+ const {
13
+ type,
14
+ payload
15
+ } = action;
16
+ switch (type) {
17
+ // Trigger navigation listeners before the `location` has been updated in Redux state.
18
+ // It doesn't matter that the new location URL has already been updated in the web browser's
19
+ // address bar, or that the web browser's history has already switched to the new locaiton.
20
+ // From the application's point of view, all of that doesn't matter and even doesn't exist.
21
+ // All that exists from the application's point of view is the `location` object in the Redux state.
22
+ // Until the `location` object in the Redux state is updated, the old page is still rendered.
23
+ // The appliation is only concerned with the updates of the `location` object in the Redux state
24
+ // and completely ignores any updates to the URL in the web browser's address bar.
25
+ case _ActionTypes.default.UPDATE:
26
+ (0, _beforeLocationChangeListeners.runBeforeLocationChangeListeners)((0, _beforeLocationChangeListeners.getBeforeLocationChangeListeners)(session), payload);
27
+ return next(action);
28
+
29
+ // Remove any navigation listeners on `DISPOSE` event.
30
+ case _ActionTypes.default.DISPOSE:
31
+ (0, _beforeLocationChangeListeners.removeAllBeforeLocationChangeListeners)(session);
32
+ return next(action);
33
+ default:
34
+ return next(action);
35
+ }
36
+ };
37
+ };
38
+ }
39
+ module.exports = exports.default;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.default = createEnvironmentMiddleware;
4
+ exports.default = createLocationMiddleware;
5
5
  var _ActionTypes = _interopRequireDefault(require("../ActionTypes"));
6
6
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
7
7
  function updateLocation(location) {
@@ -11,19 +11,17 @@ function updateLocation(location) {
11
11
  };
12
12
  }
13
13
 
14
- // Creates a "middleware" that performs the actual navigation according to the `environment` being used.
15
- // For example, when `BrowserProtocol` is used, it calls methods of the `history` object.
16
- // A better name for this function could be something like `createProtocolMiddleware(environment)`.
17
- // A better name for "environment" could be something like "environment".
18
- function createEnvironmentMiddleware(environment, {
19
- shouldIgnoreEnvironmentLocationUpdates
14
+ // Creates a "middleware" that performs the actual navigation according to the `session` being used.
15
+ // For example, when `BrowserSession` is used, it calls methods of the `window.history` object.
16
+ function createLocationMiddleware(session, {
17
+ shouldIgnoreLocationSubscriptionEvents
20
18
  }) {
21
- return function environmentMiddleware() {
19
+ return function locationMiddleware() {
22
20
  return next => {
23
21
  // Whenever browser location changes,
24
22
  // perform the same changes with the internal `location` object.
25
- const unsubscribe = environment.subscribe(location => {
26
- if (!shouldIgnoreEnvironmentLocationUpdates()) {
23
+ const unsubscribe = session.navigation.subscribe(location => {
24
+ if (!shouldIgnoreLocationSubscriptionEvents()) {
27
25
  next(updateLocation(location));
28
26
  }
29
27
  });
@@ -34,14 +32,14 @@ function createEnvironmentMiddleware(environment, {
34
32
  } = action;
35
33
  switch (type) {
36
34
  case _ActionTypes.default.INIT:
37
- return next(updateLocation(environment.init()));
35
+ return next(updateLocation(session.navigation.init()));
38
36
  case _ActionTypes.default.NAVIGATE:
39
- // `environment.navigate()` doesn't trigger the `subscribe()` listener.
40
- return next(updateLocation(environment.navigate(payload)));
37
+ // `session.navigate()` doesn't trigger the `subscribe()` listener.
38
+ return next(updateLocation(session.navigation.navigate(payload)));
41
39
  case _ActionTypes.default.SHIFT:
42
40
  // `shift()` will trigger the `subscribe()` listener,
43
41
  // which will call `updateLocation()`.
44
- environment.shift(payload);
42
+ session.navigation.shift(payload);
45
43
  // eslint-disable-next-line consistent-return
46
44
  return;
47
45
  case _ActionTypes.default.DISPOSE: