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.
- 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 +64 -59
- package/package.json +4 -4
- 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
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
exports.__esModule = true;
|
|
4
|
+
exports.default = void 0;
|
|
5
|
+
var _normalizeInputLocation = _interopRequireDefault(require("../normalizeInputLocation"));
|
|
6
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
/* eslint-disable max-classes-per-file */
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
10
|
+
function _loadState(load, isValidLoadedData) {
|
|
11
|
+
try {
|
|
12
|
+
const data = JSON.parse(load());
|
|
13
|
+
|
|
14
|
+
// Check that the stack and index at least seem reasonable before using
|
|
15
|
+
// them as state. This isn't foolproof, but it might prevent mistakes.
|
|
16
|
+
// Also perform a basic validation of `state`.
|
|
17
|
+
if (isValidLoadedData(data)) {
|
|
18
|
+
return data;
|
|
19
|
+
}
|
|
20
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
21
|
+
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
26
|
+
function _saveState(save, data) {
|
|
27
|
+
try {
|
|
28
|
+
save(JSON.stringify(data));
|
|
29
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
30
|
+
}
|
|
31
|
+
class MemoryNavigation {
|
|
32
|
+
constructor(initialLocation, {
|
|
33
|
+
save,
|
|
34
|
+
load
|
|
35
|
+
} = {}) {
|
|
36
|
+
this._save = save;
|
|
37
|
+
this._keyPrefix = Date.now().toString(36);
|
|
38
|
+
this._keyIndex = 0;
|
|
39
|
+
this._subscriptionListener = null;
|
|
40
|
+
const initialState = load ? _loadState(load, this._isValidLoadedData) : null;
|
|
41
|
+
if (initialState) {
|
|
42
|
+
this._stack = initialState.stack;
|
|
43
|
+
this._index = initialState.index;
|
|
44
|
+
} else {
|
|
45
|
+
this._stack = [Object.assign({}, (0, _normalizeInputLocation.default)(initialLocation), {
|
|
46
|
+
key: this._getNextKey()
|
|
47
|
+
})];
|
|
48
|
+
this._index = 0;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
_isValidLoadedData({
|
|
52
|
+
stack,
|
|
53
|
+
index
|
|
54
|
+
}) {
|
|
55
|
+
// Check that the `stack` and `index` at least seem reasonable before using them.
|
|
56
|
+
// This isn't foolproof, but it might prevent mistakes.
|
|
57
|
+
return Array.isArray(stack) && typeof index === 'number' && stack[index];
|
|
58
|
+
}
|
|
59
|
+
init() {
|
|
60
|
+
return this._createLocationObject({
|
|
61
|
+
action: 'INIT',
|
|
62
|
+
delta: 0
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
subscribe(listener) {
|
|
66
|
+
this._subscriptionListener = listener;
|
|
67
|
+
return () => {
|
|
68
|
+
this._subscriptionListener = null;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
navigate(location) {
|
|
72
|
+
const {
|
|
73
|
+
action,
|
|
74
|
+
pathname,
|
|
75
|
+
search,
|
|
76
|
+
query,
|
|
77
|
+
hash,
|
|
78
|
+
state
|
|
79
|
+
} = location;
|
|
80
|
+
if (action !== 'PUSH' && action !== 'REPLACE') {
|
|
81
|
+
throw Error(`Unrecognized browser session action: ${action}`);
|
|
82
|
+
}
|
|
83
|
+
const delta = action === 'PUSH' ? 1 : 0;
|
|
84
|
+
this._index += delta;
|
|
85
|
+
const key = this._getNextKey();
|
|
86
|
+
this._stack[this._index] = {
|
|
87
|
+
pathname,
|
|
88
|
+
search,
|
|
89
|
+
query,
|
|
90
|
+
hash,
|
|
91
|
+
state,
|
|
92
|
+
key
|
|
93
|
+
};
|
|
94
|
+
if (action === 'PUSH') {
|
|
95
|
+
this._stack.length = this._index + 1;
|
|
96
|
+
}
|
|
97
|
+
if (this._save) {
|
|
98
|
+
_saveState(this._save, {
|
|
99
|
+
stack: this._stack,
|
|
100
|
+
index: this._index
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return Object.assign({}, location, {
|
|
104
|
+
key,
|
|
105
|
+
index: this._index,
|
|
106
|
+
delta
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
shift(delta) {
|
|
110
|
+
const prevIndex = this._index;
|
|
111
|
+
this._index = Math.min(Math.max(this._index + delta, 0), this._stack.length - 1);
|
|
112
|
+
if (this._index === prevIndex) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (this._save) {
|
|
116
|
+
_saveState(this._save, {
|
|
117
|
+
stack: this._stack,
|
|
118
|
+
index: this._index
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (this._subscriptionListener) {
|
|
122
|
+
this._subscriptionListener(this._createLocationObject({
|
|
123
|
+
action: 'POP',
|
|
124
|
+
delta: this._index - prevIndex
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
_getNextKey() {
|
|
129
|
+
const key = `${this._keyPrefix}.${this._keyIndex.toString(36)}`;
|
|
130
|
+
this._keyIndex++;
|
|
131
|
+
return key;
|
|
132
|
+
}
|
|
133
|
+
_createLocationObject({
|
|
134
|
+
action,
|
|
135
|
+
delta
|
|
136
|
+
}) {
|
|
137
|
+
return Object.assign({}, this._stack[this._index], {
|
|
138
|
+
action,
|
|
139
|
+
index: this._index,
|
|
140
|
+
delta
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
class MemoryDataStorage {
|
|
145
|
+
constructor({
|
|
146
|
+
load,
|
|
147
|
+
save
|
|
148
|
+
} = {}) {
|
|
149
|
+
this._save = save;
|
|
150
|
+
const initialState = load ? _loadState(load, this._isValidLoadedData) : null;
|
|
151
|
+
if (initialState) {
|
|
152
|
+
this._state = initialState.state;
|
|
153
|
+
} else {
|
|
154
|
+
this._state = {};
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
159
|
+
get(key) {
|
|
160
|
+
if (key in this._state) {
|
|
161
|
+
return this._state[key];
|
|
162
|
+
}
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
remove(key) {
|
|
166
|
+
if (key in this._state) {
|
|
167
|
+
delete this._state[key];
|
|
168
|
+
}
|
|
169
|
+
if (this._save) {
|
|
170
|
+
_saveState(this._save, {
|
|
171
|
+
state: this._state
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
set(key, value) {
|
|
176
|
+
this._state[key] = value;
|
|
177
|
+
if (this._save) {
|
|
178
|
+
_saveState(this._save, {
|
|
179
|
+
state: this._state
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
_isValidLoadedData({
|
|
184
|
+
state
|
|
185
|
+
}) {
|
|
186
|
+
// Perform a basic validation of `state`.
|
|
187
|
+
return typeof state === 'object' && state !== null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function createNestedStateSaveLoadFunctions({
|
|
191
|
+
save,
|
|
192
|
+
load
|
|
193
|
+
}, key) {
|
|
194
|
+
return {
|
|
195
|
+
save: save ? data => save(key, data) : undefined,
|
|
196
|
+
load: load ? () => load(key) : undefined
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
class MemorySession {
|
|
200
|
+
constructor(initialLocation, {
|
|
201
|
+
save,
|
|
202
|
+
load
|
|
203
|
+
} = {}) {
|
|
204
|
+
this.navigation = new MemoryNavigation(initialLocation, createNestedStateSaveLoadFunctions({
|
|
205
|
+
save,
|
|
206
|
+
load
|
|
207
|
+
}, 'navigation'));
|
|
208
|
+
this.dataStorage = new MemoryDataStorage(createNestedStateSaveLoadFunctions({
|
|
209
|
+
save,
|
|
210
|
+
load
|
|
211
|
+
}, 'dataStorage'));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// "Before destroy" listeners are currently ignored.
|
|
215
|
+
// If required, one could implement a `_destroy()` method
|
|
216
|
+
// and there check that the listeners actually do get called.
|
|
217
|
+
// eslint-disable-next-line no-unused-vars
|
|
218
|
+
addBeforeDestroyListener(listener) {
|
|
219
|
+
return () => {};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
exports.default = MemorySession;
|
|
223
|
+
module.exports = exports.default;
|
|
@@ -4,50 +4,62 @@ exports.__esModule = true;
|
|
|
4
4
|
exports.default = void 0;
|
|
5
5
|
var _normalizeInputLocation = _interopRequireDefault(require("../normalizeInputLocation"));
|
|
6
6
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
/* eslint-disable max-classes-per-file */
|
|
8
|
+
|
|
7
9
|
function noop() {}
|
|
8
10
|
function serverSideNavigationNotPossible() {
|
|
9
11
|
throw new Error('Server-side navigation is not possible');
|
|
10
12
|
}
|
|
11
|
-
class
|
|
13
|
+
class ServerNavigation {
|
|
12
14
|
constructor(initialLocation) {
|
|
13
15
|
this._location = (0, _normalizeInputLocation.default)(initialLocation);
|
|
14
16
|
}
|
|
15
17
|
init() {
|
|
16
18
|
return Object.assign({
|
|
17
|
-
action: '
|
|
18
|
-
}, this._location
|
|
19
|
+
action: 'INIT'
|
|
20
|
+
}, this._location, {
|
|
21
|
+
index: 0,
|
|
22
|
+
key: '0'
|
|
23
|
+
});
|
|
19
24
|
}
|
|
20
25
|
subscribe() {
|
|
21
|
-
// Server environment emits no location events.
|
|
26
|
+
// Server-side environment emits no location subscription events.
|
|
22
27
|
return noop;
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
// Navigation methods are not implemented, because `
|
|
30
|
+
// Navigation methods are not implemented, because `ServerSession` instances
|
|
26
31
|
// cannot navigate.
|
|
27
32
|
navigate() {
|
|
28
33
|
serverSideNavigationNotPossible();
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
// Navigation methods are not implemented, because `
|
|
36
|
+
// Navigation methods are not implemented, because `ServerSession` instances
|
|
32
37
|
// cannot navigate.
|
|
33
38
|
shift() {
|
|
34
39
|
serverSideNavigationNotPossible();
|
|
35
40
|
}
|
|
41
|
+
}
|
|
42
|
+
class ServerDataStorage {
|
|
43
|
+
// It doesn't seem to make any sense to store anything on server side.
|
|
44
|
+
// Hence, state management methods are "no op" stubs.
|
|
45
|
+
get() {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
remove() {}
|
|
49
|
+
set() {}
|
|
50
|
+
}
|
|
51
|
+
class ServerSession {
|
|
52
|
+
constructor(initialLocation) {
|
|
53
|
+
this.navigation = new ServerNavigation(initialLocation);
|
|
54
|
+
this.dataStorage = new ServerDataStorage();
|
|
55
|
+
}
|
|
36
56
|
|
|
37
57
|
// "Before destroy" listeners are currently ignored.
|
|
38
58
|
// If required, one could implement a `_destroy()` method
|
|
39
59
|
// and there check that the listeners actually do get called.
|
|
40
60
|
addBeforeDestroyListener() {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// It doesn't seem to make any sense to store anything on server side.
|
|
45
|
-
// Hence, state management methods are "no op" stubs.
|
|
46
|
-
getState() {
|
|
47
|
-
return null;
|
|
61
|
+
return noop;
|
|
48
62
|
}
|
|
49
|
-
removeState() {}
|
|
50
|
-
setState() {}
|
|
51
63
|
}
|
|
52
|
-
exports.default =
|
|
64
|
+
exports.default = ServerSession;
|
|
53
65
|
module.exports = exports.default;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import getLocationUrl from './getLocationUrl';
|
|
2
|
-
export default class
|
|
2
|
+
export default class LocationDataStorage {
|
|
3
3
|
constructor(environment, {
|
|
4
4
|
namespace
|
|
5
5
|
} = {}) {
|
|
@@ -10,7 +10,7 @@ export default class LocationStateStorage {
|
|
|
10
10
|
get(location, key) {
|
|
11
11
|
const stateKey = this._getStateKey(location, key);
|
|
12
12
|
try {
|
|
13
|
-
const value = this._environment.
|
|
13
|
+
const value = this._environment.dataStorage.get(stateKey);
|
|
14
14
|
// === null is probably sufficient.
|
|
15
15
|
if (value === null) {
|
|
16
16
|
return undefined;
|
|
@@ -28,7 +28,7 @@ export default class LocationStateStorage {
|
|
|
28
28
|
const stateKey = this._getStateKey(location, key);
|
|
29
29
|
if (value === undefined) {
|
|
30
30
|
try {
|
|
31
|
-
this._environment.
|
|
31
|
+
this._environment.dataStorage.remove(stateKey);
|
|
32
32
|
} catch (error) {
|
|
33
33
|
// No need to handle errors here.
|
|
34
34
|
}
|
|
@@ -39,7 +39,7 @@ export default class LocationStateStorage {
|
|
|
39
39
|
// value here is provided by the caller of this method.
|
|
40
40
|
const valueString = JSON.stringify(value);
|
|
41
41
|
try {
|
|
42
|
-
this._environment.
|
|
42
|
+
this._environment.dataStorage.set(stateKey, valueString);
|
|
43
43
|
} catch (error) {
|
|
44
44
|
// No need to handle errors here either. If it didn't work, it didn't
|
|
45
45
|
// work. We make no guarantees about actually saving the value.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
|
|
3
|
+
export function getBeforeLocationChangeListeners(session) {
|
|
4
|
+
return session._beforeLocationChangeListenersList || [];
|
|
5
|
+
}
|
|
6
|
+
function addBeforeLocationChangeListenerToTheList(listener, session) {
|
|
7
|
+
if (!session._beforeLocationChangeListenersList) {
|
|
8
|
+
session._beforeLocationChangeListenersList = [];
|
|
9
|
+
}
|
|
10
|
+
session._beforeLocationChangeListenersList.push(listener);
|
|
11
|
+
}
|
|
12
|
+
function removeBeforeLocationChangeListenerFromTheList(listener, session) {
|
|
13
|
+
if (session._beforeLocationChangeListenersList) {
|
|
14
|
+
session._beforeLocationChangeListenersList = session._beforeLocationChangeListenersList.filter(_ => _ !== listener);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function removeAllBeforeLocationChangeListeners(session) {
|
|
18
|
+
session._beforeLocationChangeListenersList = [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Runs the `listener` while ignoring any errors that might be thrown by it.
|
|
22
|
+
function runBeforeLocationChangeListener(listener, location) {
|
|
23
|
+
try {
|
|
24
|
+
listener(location);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// eslint-disable-next-line no-console
|
|
27
|
+
console.warn(`Ignoring before location change listener \`${listener.name}\` that failed with \`${error}\`.`);
|
|
28
|
+
// eslint-disable-next-line no-console
|
|
29
|
+
console.error(error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Runs all listeners in order.
|
|
34
|
+
export function runBeforeLocationChangeListeners(navigationListeners, toLocation) {
|
|
35
|
+
for (const listener of navigationListeners) {
|
|
36
|
+
runBeforeLocationChangeListener(listener, toLocation);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function addBeforeLocationChangeListener(session, listener) {
|
|
40
|
+
addBeforeLocationChangeListenerToTheList(listener, session);
|
|
41
|
+
return () => {
|
|
42
|
+
removeBeforeLocationChangeListenerFromTheList(listener, session);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import createBasePathMiddleware from './middleware/createBasePathMiddleware';
|
|
2
|
-
import
|
|
2
|
+
import createBeforeLocationChangeListenerMiddleware from './middleware/createBeforeLocationChangeListenerMiddleware';
|
|
3
|
+
import createLocationMiddleware from './middleware/createLocationMiddleware';
|
|
3
4
|
import createNavigationBlockerMiddleware from './middleware/createNavigationBlockerMiddleware';
|
|
4
5
|
import navigationActionMiddleware from './middleware/navigationActionMiddleware';
|
|
5
6
|
import normalizeInputLocationMiddleware from './middleware/normalizeInputLocationMiddleware';
|
|
6
|
-
export default function createMiddlewares(
|
|
7
|
-
// Allows temporarily ignoring
|
|
8
|
-
let
|
|
9
|
-
const
|
|
10
|
-
|
|
7
|
+
export default function createMiddlewares(session, options) {
|
|
8
|
+
// Allows temporarily ignoring location update events.
|
|
9
|
+
let shouldIgnoreLocationSubscriptionEvents = false;
|
|
10
|
+
const ignoreLocationSubscriptionEvents = func => {
|
|
11
|
+
shouldIgnoreLocationSubscriptionEvents = true;
|
|
11
12
|
func();
|
|
12
|
-
|
|
13
|
+
shouldIgnoreLocationSubscriptionEvents = false;
|
|
13
14
|
};
|
|
14
15
|
return [
|
|
15
16
|
// Validates that the action "payload" (input location) is a proper `NormalizedInputLocation`.
|
|
@@ -21,17 +22,20 @@ export default function createMiddlewares(environment, options) {
|
|
|
21
22
|
createBasePathMiddleware(options && options.basePath),
|
|
22
23
|
// Allows blocking navigation.
|
|
23
24
|
// Handles `NAVIGATE` actions dispatched by the application itself.
|
|
24
|
-
createNavigationBlockerMiddleware(
|
|
25
|
-
|
|
25
|
+
createNavigationBlockerMiddleware(session, {
|
|
26
|
+
ignoreLocationSubscriptionEvents
|
|
26
27
|
}),
|
|
27
|
-
// This "middleware" performs the actual navigation according to the `
|
|
28
|
-
// For example, when `
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
// This "middleware" performs the actual navigation according to the `session` being used.
|
|
29
|
+
// For example, when `BrowserSession` is used, it calls methods of the `history` object.
|
|
30
|
+
createLocationMiddleware(session, {
|
|
31
|
+
shouldIgnoreLocationSubscriptionEvents: () => shouldIgnoreLocationSubscriptionEvents
|
|
31
32
|
}),
|
|
32
33
|
// Allows blocking navigation.
|
|
33
|
-
// Handles location `UPDATE` actions dispatched
|
|
34
|
-
createNavigationBlockerMiddleware(
|
|
35
|
-
|
|
36
|
-
})
|
|
34
|
+
// Handles location `UPDATE` actions dispatched in response to location update events.
|
|
35
|
+
createNavigationBlockerMiddleware(session, {
|
|
36
|
+
ignoreLocationSubscriptionEvents
|
|
37
|
+
}),
|
|
38
|
+
// Allows subscribing to upcoming location changes
|
|
39
|
+
// before those changes are applied in the `location` object in the state.
|
|
40
|
+
createBeforeLocationChangeListenerMiddleware(session)];
|
|
37
41
|
}
|
package/lib/esm/index.js
CHANGED
|
@@ -6,7 +6,7 @@ export { default as getLocationUrl } from './getLocationUrl';
|
|
|
6
6
|
export { default as parseLocationUrl } from './parseLocationUrl';
|
|
7
7
|
export { default as createMiddlewares } from './createMiddlewares';
|
|
8
8
|
export { default as locationReducer } from './locationReducer';
|
|
9
|
-
export { default as
|
|
10
|
-
export { default as
|
|
11
|
-
export { default as
|
|
12
|
-
export { default as
|
|
9
|
+
export { default as LocationDataStorage } from './LocationDataStorage';
|
|
10
|
+
export { default as BrowserSession } from './session/BrowserSession';
|
|
11
|
+
export { default as MemorySession } from './session/MemorySession';
|
|
12
|
+
export { default as ServerSession } from './session/ServerSession';
|
|
@@ -10,9 +10,9 @@ export default function createBasePathMiddleware(basePath) {
|
|
|
10
10
|
transformInputLocation: location => {
|
|
11
11
|
return addBasePath(location, basePath);
|
|
12
12
|
},
|
|
13
|
-
// Transforms
|
|
13
|
+
// Transforms subscription `Location` object:
|
|
14
14
|
// removes `basePath` from the URL.
|
|
15
|
-
|
|
15
|
+
transformSubscriptionLocation: location => {
|
|
16
16
|
return removeBasePath(location, basePath);
|
|
17
17
|
}
|
|
18
18
|
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import ActionTypes from '../ActionTypes';
|
|
2
|
+
import { getBeforeLocationChangeListeners, removeAllBeforeLocationChangeListeners, runBeforeLocationChangeListeners } from '../beforeLocationChangeListeners';
|
|
3
|
+
|
|
4
|
+
// Creates a "middleware" that calls upcoming navigation listeners.
|
|
5
|
+
export default function createBeforeLocationChangeListenerMiddleware(session) {
|
|
6
|
+
return function navigationListenerMiddleware() {
|
|
7
|
+
return next => action => {
|
|
8
|
+
const {
|
|
9
|
+
type,
|
|
10
|
+
payload
|
|
11
|
+
} = action;
|
|
12
|
+
switch (type) {
|
|
13
|
+
// Trigger navigation listeners before the `location` has been updated in Redux state.
|
|
14
|
+
// It doesn't matter that the new location URL has already been updated in the web browser's
|
|
15
|
+
// address bar, or that the web browser's history has already switched to the new locaiton.
|
|
16
|
+
// From the application's point of view, all of that doesn't matter and even doesn't exist.
|
|
17
|
+
// All that exists from the application's point of view is the `location` object in the Redux state.
|
|
18
|
+
// Until the `location` object in the Redux state is updated, the old page is still rendered.
|
|
19
|
+
// The appliation is only concerned with the updates of the `location` object in the Redux state
|
|
20
|
+
// and completely ignores any updates to the URL in the web browser's address bar.
|
|
21
|
+
case ActionTypes.UPDATE:
|
|
22
|
+
runBeforeLocationChangeListeners(getBeforeLocationChangeListeners(session), payload);
|
|
23
|
+
return next(action);
|
|
24
|
+
|
|
25
|
+
// Remove any navigation listeners on `DISPOSE` event.
|
|
26
|
+
case ActionTypes.DISPOSE:
|
|
27
|
+
removeAllBeforeLocationChangeListeners(session);
|
|
28
|
+
return next(action);
|
|
29
|
+
default:
|
|
30
|
+
return next(action);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -6,19 +6,17 @@ function updateLocation(location) {
|
|
|
6
6
|
};
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
// Creates a "middleware" that performs the actual navigation according to the `
|
|
10
|
-
// For example, when `
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export default function createEnvironmentMiddleware(environment, {
|
|
14
|
-
shouldIgnoreEnvironmentLocationUpdates
|
|
9
|
+
// Creates a "middleware" that performs the actual navigation according to the `session` being used.
|
|
10
|
+
// For example, when `BrowserSession` is used, it calls methods of the `window.history` object.
|
|
11
|
+
export default function createLocationMiddleware(session, {
|
|
12
|
+
shouldIgnoreLocationSubscriptionEvents
|
|
15
13
|
}) {
|
|
16
|
-
return function
|
|
14
|
+
return function locationMiddleware() {
|
|
17
15
|
return next => {
|
|
18
16
|
// Whenever browser location changes,
|
|
19
17
|
// perform the same changes with the internal `location` object.
|
|
20
|
-
const unsubscribe =
|
|
21
|
-
if (!
|
|
18
|
+
const unsubscribe = session.navigation.subscribe(location => {
|
|
19
|
+
if (!shouldIgnoreLocationSubscriptionEvents()) {
|
|
22
20
|
next(updateLocation(location));
|
|
23
21
|
}
|
|
24
22
|
});
|
|
@@ -29,14 +27,14 @@ export default function createEnvironmentMiddleware(environment, {
|
|
|
29
27
|
} = action;
|
|
30
28
|
switch (type) {
|
|
31
29
|
case ActionTypes.INIT:
|
|
32
|
-
return next(updateLocation(
|
|
30
|
+
return next(updateLocation(session.navigation.init()));
|
|
33
31
|
case ActionTypes.NAVIGATE:
|
|
34
|
-
// `
|
|
35
|
-
return next(updateLocation(
|
|
32
|
+
// `session.navigate()` doesn't trigger the `subscribe()` listener.
|
|
33
|
+
return next(updateLocation(session.navigation.navigate(payload)));
|
|
36
34
|
case ActionTypes.SHIFT:
|
|
37
35
|
// `shift()` will trigger the `subscribe()` listener,
|
|
38
36
|
// which will call `updateLocation()`.
|
|
39
|
-
|
|
37
|
+
session.navigation.shift(payload);
|
|
40
38
|
// eslint-disable-next-line consistent-return
|
|
41
39
|
return;
|
|
42
40
|
case ActionTypes.DISPOSE:
|