navigation-stack 0.1.2 → 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.
- package/.github/workflows/main.yml +39 -39
- package/README.md +128 -27
- package/lib/cjs/{LocationStateStorage.js → LocationDataStorage.js} +5 -5
- package/lib/cjs/addBeforeLocationChangeListener.js +7 -0
- package/lib/cjs/beforeLocationChangeListeners.js +51 -0
- package/lib/cjs/createMiddlewares.js +21 -17
- package/lib/cjs/index.js +9 -9
- package/lib/cjs/middleware/createBasePathMiddleware.js +2 -2
- package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +39 -0
- package/lib/cjs/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +62 -29
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +2 -2
- package/lib/cjs/navigationBlockers.js +55 -47
- package/lib/cjs/normalizeInputLocation.js +1 -0
- package/lib/cjs/parseLocationUrl.js +2 -0
- package/lib/cjs/parseQueryFromSearch.js +1 -1
- package/lib/cjs/session/BrowserSession.js +229 -0
- package/lib/cjs/session/MemorySession.js +223 -0
- package/lib/cjs/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -16
- package/lib/esm/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
- package/lib/esm/addBeforeLocationChangeListener.js +2 -0
- package/lib/esm/beforeLocationChangeListeners.js +44 -0
- package/lib/esm/createMiddlewares.js +21 -17
- package/lib/esm/index.js +4 -4
- package/lib/esm/middleware/createBasePathMiddleware.js +2 -2
- package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +34 -0
- package/lib/esm/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +11 -13
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +62 -29
- package/lib/esm/middleware/createTransformLocationMiddleware.js +2 -2
- package/lib/esm/navigationBlockers.js +55 -47
- package/lib/esm/normalizeInputLocation.js +1 -0
- package/lib/esm/parseLocationUrl.js +2 -0
- package/lib/esm/parseQueryFromSearch.js +1 -1
- package/lib/esm/session/BrowserSession.js +223 -0
- package/lib/esm/session/MemorySession.js +217 -0
- package/lib/esm/{environment/ServerEnvironment.js → session/ServerSession.js} +27 -15
- package/lib/index.d.ts +66 -61
- package/package.json +5 -5
- package/src/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
- package/src/addBeforeLocationChangeListener.js +2 -0
- package/src/beforeLocationChangeListeners.js +54 -0
- package/src/createMiddlewares.js +21 -17
- package/src/index.js +4 -4
- package/src/middleware/createBasePathMiddleware.js +2 -2
- package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +40 -0
- package/src/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
- package/src/middleware/createNavigationBlockerMiddleware.js +68 -28
- package/src/middleware/createTransformLocationMiddleware.js +2 -2
- package/src/navigationBlockers.js +68 -49
- package/src/normalizeInputLocation.js +1 -0
- package/src/parseLocationUrl.js +2 -0
- package/src/parseQueryFromSearch.js +1 -1
- package/src/session/BrowserSession.js +225 -0
- package/src/session/MemorySession.js +219 -0
- package/src/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -15
- package/test/{LocationStateStorage.test.js → LocationDataStorage.test.js} +6 -6
- package/test/createMiddlewares.test.js +2 -2
- package/test/helpers.js +1 -1
- package/test/index.test.js +3 -3
- package/test/middleware/createBasePathMiddleware.test.js +7 -7
- package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +141 -0
- package/test/middleware/createNavigationBlockerMiddleware.test.js +96 -97
- package/test/middleware/createTransformLocationMiddleware.test.js +1 -1
- package/test/normalizeInputLocation.test.js +3 -0
- package/test/parseLocationUrl.test.js +2 -0
- package/test/{environment/BrowserEnvironment.test.js → session/BrowserSession.test.js} +35 -18
- package/test/session/MemorySession.test.js +244 -0
- package/test/session/ServerSession.test.js +23 -0
- package/types/index.d.ts +64 -59
- package/lib/cjs/environment/BrowserEnvironment.js +0 -111
- package/lib/cjs/environment/MemoryEnvironment.js +0 -150
- package/lib/esm/environment/BrowserEnvironment.js +0 -104
- package/lib/esm/environment/MemoryEnvironment.js +0 -143
- package/src/environment/BrowserEnvironment.js +0 -109
- package/src/environment/MemoryEnvironment.js +0 -151
- package/test/environment/MemoryEnvironment.test.js +0 -218
- 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
|
-
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
152
|
+
BrowserSession,
|
|
153
|
+
ServerSession,
|
|
154
|
+
MemorySession
|
|
106
155
|
} from 'navigation-stack'
|
|
107
156
|
|
|
108
|
-
new
|
|
109
|
-
new
|
|
110
|
-
new
|
|
157
|
+
new BrowserSession()
|
|
158
|
+
new ServerSession('/initial-location-url')
|
|
159
|
+
new MemorySession('/initial-location-url')
|
|
111
160
|
```
|
|
112
161
|
|
|
113
|
-
- Use `
|
|
114
|
-
- Use `
|
|
115
|
-
- Use `
|
|
116
|
-
- `
|
|
117
|
-
- `save(
|
|
118
|
-
- `load()` — Loads
|
|
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(
|
|
174
|
+
createMiddlewares(session, { basePath?: '/base/path' })
|
|
126
175
|
```
|
|
127
176
|
|
|
128
177
|
## Location State Storage
|
|
129
178
|
|
|
130
|
-
One could use
|
|
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 {
|
|
182
|
+
import { BrowserSession, LocationDataStorage } from 'navigation-stack'
|
|
134
183
|
|
|
135
|
-
const
|
|
184
|
+
const session = new BrowserSession()
|
|
136
185
|
|
|
137
|
-
const storage = new
|
|
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
|
-
`
|
|
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
|
-
|
|
252
|
+
BrowserSession,
|
|
157
253
|
addNavigationBlocker
|
|
158
254
|
} from 'navigation-stack'
|
|
159
255
|
|
|
160
|
-
const
|
|
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(
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
|
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(
|
|
12
|
-
// Allows temporarily ignoring
|
|
13
|
-
let
|
|
14
|
-
const
|
|
15
|
-
|
|
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
|
-
|
|
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)(
|
|
30
|
-
|
|
30
|
+
(0, _createNavigationBlockerMiddleware.default)(session, {
|
|
31
|
+
ignoreLocationSubscriptionEvents
|
|
31
32
|
}),
|
|
32
|
-
// This "middleware" performs the actual navigation according to the `
|
|
33
|
-
// For example, when `
|
|
34
|
-
(0,
|
|
35
|
-
|
|
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
|
|
39
|
-
(0, _createNavigationBlockerMiddleware.default)(
|
|
40
|
-
|
|
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.
|
|
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
|
|
23
|
-
exports.
|
|
24
|
-
var
|
|
25
|
-
exports.
|
|
26
|
-
var
|
|
27
|
-
exports.
|
|
28
|
-
var
|
|
29
|
-
exports.
|
|
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
|
|
17
|
+
// Transforms subscription `Location` object:
|
|
18
18
|
// removes `basePath` from the URL.
|
|
19
|
-
|
|
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 =
|
|
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 `
|
|
15
|
-
// For example, when `
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
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 =
|
|
26
|
-
if (!
|
|
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(
|
|
35
|
+
return next(updateLocation(session.navigation.init()));
|
|
38
36
|
case _ActionTypes.default.NAVIGATE:
|
|
39
|
-
// `
|
|
40
|
-
return next(updateLocation(
|
|
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
|
-
|
|
42
|
+
session.navigation.shift(payload);
|
|
45
43
|
// eslint-disable-next-line consistent-return
|
|
46
44
|
return;
|
|
47
45
|
case _ActionTypes.default.DISPOSE:
|