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,217 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
|
|
3
|
+
import normalizeInputLocation from '../normalizeInputLocation';
|
|
4
|
+
|
|
5
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
6
|
+
function _loadState(load, isValidLoadedData) {
|
|
7
|
+
try {
|
|
8
|
+
const data = JSON.parse(load());
|
|
9
|
+
|
|
10
|
+
// Check that the stack and index at least seem reasonable before using
|
|
11
|
+
// them as state. This isn't foolproof, but it might prevent mistakes.
|
|
12
|
+
// Also perform a basic validation of `state`.
|
|
13
|
+
if (isValidLoadedData(data)) {
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
16
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
17
|
+
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// eslint-disable-next-line no-underscore-dangle
|
|
22
|
+
function _saveState(save, data) {
|
|
23
|
+
try {
|
|
24
|
+
save(JSON.stringify(data));
|
|
25
|
+
} catch (error) {} // eslint-disable-line no-empty
|
|
26
|
+
}
|
|
27
|
+
class MemoryNavigation {
|
|
28
|
+
constructor(initialLocation, {
|
|
29
|
+
save,
|
|
30
|
+
load
|
|
31
|
+
} = {}) {
|
|
32
|
+
this._save = save;
|
|
33
|
+
this._keyPrefix = Date.now().toString(36);
|
|
34
|
+
this._keyIndex = 0;
|
|
35
|
+
this._subscriptionListener = null;
|
|
36
|
+
const initialState = load ? _loadState(load, this._isValidLoadedData) : null;
|
|
37
|
+
if (initialState) {
|
|
38
|
+
this._stack = initialState.stack;
|
|
39
|
+
this._index = initialState.index;
|
|
40
|
+
} else {
|
|
41
|
+
this._stack = [Object.assign({}, normalizeInputLocation(initialLocation), {
|
|
42
|
+
key: this._getNextKey()
|
|
43
|
+
})];
|
|
44
|
+
this._index = 0;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
_isValidLoadedData({
|
|
48
|
+
stack,
|
|
49
|
+
index
|
|
50
|
+
}) {
|
|
51
|
+
// Check that the `stack` and `index` at least seem reasonable before using them.
|
|
52
|
+
// This isn't foolproof, but it might prevent mistakes.
|
|
53
|
+
return Array.isArray(stack) && typeof index === 'number' && stack[index];
|
|
54
|
+
}
|
|
55
|
+
init() {
|
|
56
|
+
return this._createLocationObject({
|
|
57
|
+
action: 'INIT',
|
|
58
|
+
delta: 0
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
subscribe(listener) {
|
|
62
|
+
this._subscriptionListener = listener;
|
|
63
|
+
return () => {
|
|
64
|
+
this._subscriptionListener = null;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
navigate(location) {
|
|
68
|
+
const {
|
|
69
|
+
action,
|
|
70
|
+
pathname,
|
|
71
|
+
search,
|
|
72
|
+
query,
|
|
73
|
+
hash,
|
|
74
|
+
state
|
|
75
|
+
} = location;
|
|
76
|
+
if (action !== 'PUSH' && action !== 'REPLACE') {
|
|
77
|
+
throw Error(`Unrecognized browser session action: ${action}`);
|
|
78
|
+
}
|
|
79
|
+
const delta = action === 'PUSH' ? 1 : 0;
|
|
80
|
+
this._index += delta;
|
|
81
|
+
const key = this._getNextKey();
|
|
82
|
+
this._stack[this._index] = {
|
|
83
|
+
pathname,
|
|
84
|
+
search,
|
|
85
|
+
query,
|
|
86
|
+
hash,
|
|
87
|
+
state,
|
|
88
|
+
key
|
|
89
|
+
};
|
|
90
|
+
if (action === 'PUSH') {
|
|
91
|
+
this._stack.length = this._index + 1;
|
|
92
|
+
}
|
|
93
|
+
if (this._save) {
|
|
94
|
+
_saveState(this._save, {
|
|
95
|
+
stack: this._stack,
|
|
96
|
+
index: this._index
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return Object.assign({}, location, {
|
|
100
|
+
key,
|
|
101
|
+
index: this._index,
|
|
102
|
+
delta
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
shift(delta) {
|
|
106
|
+
const prevIndex = this._index;
|
|
107
|
+
this._index = Math.min(Math.max(this._index + delta, 0), this._stack.length - 1);
|
|
108
|
+
if (this._index === prevIndex) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (this._save) {
|
|
112
|
+
_saveState(this._save, {
|
|
113
|
+
stack: this._stack,
|
|
114
|
+
index: this._index
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (this._subscriptionListener) {
|
|
118
|
+
this._subscriptionListener(this._createLocationObject({
|
|
119
|
+
action: 'POP',
|
|
120
|
+
delta: this._index - prevIndex
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
_getNextKey() {
|
|
125
|
+
const key = `${this._keyPrefix}.${this._keyIndex.toString(36)}`;
|
|
126
|
+
this._keyIndex++;
|
|
127
|
+
return key;
|
|
128
|
+
}
|
|
129
|
+
_createLocationObject({
|
|
130
|
+
action,
|
|
131
|
+
delta
|
|
132
|
+
}) {
|
|
133
|
+
return Object.assign({}, this._stack[this._index], {
|
|
134
|
+
action,
|
|
135
|
+
index: this._index,
|
|
136
|
+
delta
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
class MemoryDataStorage {
|
|
141
|
+
constructor({
|
|
142
|
+
load,
|
|
143
|
+
save
|
|
144
|
+
} = {}) {
|
|
145
|
+
this._save = save;
|
|
146
|
+
const initialState = load ? _loadState(load, this._isValidLoadedData) : null;
|
|
147
|
+
if (initialState) {
|
|
148
|
+
this._state = initialState.state;
|
|
149
|
+
} else {
|
|
150
|
+
this._state = {};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Returns either a `string` value or `null` if the key doesn't exist.
|
|
155
|
+
get(key) {
|
|
156
|
+
if (key in this._state) {
|
|
157
|
+
return this._state[key];
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
remove(key) {
|
|
162
|
+
if (key in this._state) {
|
|
163
|
+
delete this._state[key];
|
|
164
|
+
}
|
|
165
|
+
if (this._save) {
|
|
166
|
+
_saveState(this._save, {
|
|
167
|
+
state: this._state
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
set(key, value) {
|
|
172
|
+
this._state[key] = value;
|
|
173
|
+
if (this._save) {
|
|
174
|
+
_saveState(this._save, {
|
|
175
|
+
state: this._state
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
_isValidLoadedData({
|
|
180
|
+
state
|
|
181
|
+
}) {
|
|
182
|
+
// Perform a basic validation of `state`.
|
|
183
|
+
return typeof state === 'object' && state !== null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function createNestedStateSaveLoadFunctions({
|
|
187
|
+
save,
|
|
188
|
+
load
|
|
189
|
+
}, key) {
|
|
190
|
+
return {
|
|
191
|
+
save: save ? data => save(key, data) : undefined,
|
|
192
|
+
load: load ? () => load(key) : undefined
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
export default class MemorySession {
|
|
196
|
+
constructor(initialLocation, {
|
|
197
|
+
save,
|
|
198
|
+
load
|
|
199
|
+
} = {}) {
|
|
200
|
+
this.navigation = new MemoryNavigation(initialLocation, createNestedStateSaveLoadFunctions({
|
|
201
|
+
save,
|
|
202
|
+
load
|
|
203
|
+
}, 'navigation'));
|
|
204
|
+
this.dataStorage = new MemoryDataStorage(createNestedStateSaveLoadFunctions({
|
|
205
|
+
save,
|
|
206
|
+
load
|
|
207
|
+
}, 'dataStorage'));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// "Before destroy" listeners are currently ignored.
|
|
211
|
+
// If required, one could implement a `_destroy()` method
|
|
212
|
+
// and there check that the listeners actually do get called.
|
|
213
|
+
// eslint-disable-next-line no-unused-vars
|
|
214
|
+
addBeforeDestroyListener(listener) {
|
|
215
|
+
return () => {};
|
|
216
|
+
}
|
|
217
|
+
}
|
|
@@ -1,46 +1,58 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
|
|
1
3
|
import normalizeInputLocation from '../normalizeInputLocation';
|
|
2
4
|
function noop() {}
|
|
3
5
|
function serverSideNavigationNotPossible() {
|
|
4
6
|
throw new Error('Server-side navigation is not possible');
|
|
5
7
|
}
|
|
6
|
-
|
|
8
|
+
class ServerNavigation {
|
|
7
9
|
constructor(initialLocation) {
|
|
8
10
|
this._location = normalizeInputLocation(initialLocation);
|
|
9
11
|
}
|
|
10
12
|
init() {
|
|
11
13
|
return Object.assign({
|
|
12
|
-
action: '
|
|
13
|
-
}, this._location
|
|
14
|
+
action: 'INIT'
|
|
15
|
+
}, this._location, {
|
|
16
|
+
index: 0,
|
|
17
|
+
key: '0'
|
|
18
|
+
});
|
|
14
19
|
}
|
|
15
20
|
subscribe() {
|
|
16
|
-
// Server environment emits no location events.
|
|
21
|
+
// Server-side environment emits no location subscription events.
|
|
17
22
|
return noop;
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
// Navigation methods are not implemented, because `
|
|
25
|
+
// Navigation methods are not implemented, because `ServerSession` instances
|
|
21
26
|
// cannot navigate.
|
|
22
27
|
navigate() {
|
|
23
28
|
serverSideNavigationNotPossible();
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
// Navigation methods are not implemented, because `
|
|
31
|
+
// Navigation methods are not implemented, because `ServerSession` instances
|
|
27
32
|
// cannot navigate.
|
|
28
33
|
shift() {
|
|
29
34
|
serverSideNavigationNotPossible();
|
|
30
35
|
}
|
|
36
|
+
}
|
|
37
|
+
class ServerDataStorage {
|
|
38
|
+
// It doesn't seem to make any sense to store anything on server side.
|
|
39
|
+
// Hence, state management methods are "no op" stubs.
|
|
40
|
+
get() {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
remove() {}
|
|
44
|
+
set() {}
|
|
45
|
+
}
|
|
46
|
+
export default class ServerSession {
|
|
47
|
+
constructor(initialLocation) {
|
|
48
|
+
this.navigation = new ServerNavigation(initialLocation);
|
|
49
|
+
this.dataStorage = new ServerDataStorage();
|
|
50
|
+
}
|
|
31
51
|
|
|
32
52
|
// "Before destroy" listeners are currently ignored.
|
|
33
53
|
// If required, one could implement a `_destroy()` method
|
|
34
54
|
// and there check that the listeners actually do get called.
|
|
35
55
|
addBeforeDestroyListener() {
|
|
36
|
-
return
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// It doesn't seem to make any sense to store anything on server side.
|
|
40
|
-
// Hence, state management methods are "no op" stubs.
|
|
41
|
-
getState() {
|
|
42
|
-
return null;
|
|
56
|
+
return noop;
|
|
43
57
|
}
|
|
44
|
-
removeState() {}
|
|
45
|
-
setState() {}
|
|
46
58
|
}
|
package/lib/index.d.ts
CHANGED
|
@@ -13,12 +13,9 @@ export type InputLocationQuery = Record<
|
|
|
13
13
|
|
|
14
14
|
export interface Location<TState = any> {
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
17
|
-
* FarceActions.replace respectively; 'POP' on the initial location, or if
|
|
18
|
-
* the location was reached via the browser back or forward buttons or
|
|
19
|
-
* via FarceActions.shift
|
|
16
|
+
* See the README on the `action` property of `location`.
|
|
20
17
|
*/
|
|
21
|
-
action: 'PUSH' | 'REPLACE' | 'POP';
|
|
18
|
+
action: 'PUSH' | 'REPLACE' | 'POP' | 'INIT';
|
|
22
19
|
/**
|
|
23
20
|
* the path name; as on window.location e.g. '/foo'
|
|
24
21
|
*/
|
|
@@ -36,9 +33,9 @@ export interface Location<TState = any> {
|
|
|
36
33
|
*/
|
|
37
34
|
hash: string;
|
|
38
35
|
/**
|
|
39
|
-
*
|
|
36
|
+
* a unique key identifying the current history entry
|
|
40
37
|
*/
|
|
41
|
-
key
|
|
38
|
+
key: string;
|
|
42
39
|
/**
|
|
43
40
|
* the current index of the history entry, starting at 0 for the initial
|
|
44
41
|
* entry; this increments on FarceActions.push but not on
|
|
@@ -46,12 +43,11 @@ export interface Location<TState = any> {
|
|
|
46
43
|
*/
|
|
47
44
|
index: number;
|
|
48
45
|
/**
|
|
49
|
-
* the difference between the current index and the index of the previous
|
|
50
|
-
* location
|
|
46
|
+
* the difference between the current index and the index of the previous location
|
|
51
47
|
*/
|
|
52
48
|
delta: number;
|
|
53
49
|
/**
|
|
54
|
-
* additional location state that
|
|
50
|
+
* any additional location state that the application might explicitly define and store
|
|
55
51
|
*/
|
|
56
52
|
state: TState;
|
|
57
53
|
}
|
|
@@ -70,11 +66,15 @@ export interface InputLocationObject {
|
|
|
70
66
|
export interface LocationBase {
|
|
71
67
|
pathname: Location['pathname'];
|
|
72
68
|
search: Location['search'];
|
|
73
|
-
query
|
|
69
|
+
query: Query;
|
|
74
70
|
hash: Location['hash'];
|
|
75
71
|
state?: Location['state'];
|
|
76
72
|
}
|
|
77
73
|
|
|
74
|
+
export interface NavigationLocation extends LocationBase {
|
|
75
|
+
action: 'PUSH' | 'REPLACE';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
78
|
/**
|
|
79
79
|
* Location descriptor string:
|
|
80
80
|
* store.dispatch(FarceActions.push('/foo?bar=baz#qux'));
|
|
@@ -109,13 +109,20 @@ export type NavigationBlockerResult =
|
|
|
109
109
|
| Promise<NavigationBlockerSyncResult>;
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
*
|
|
113
|
-
* is attempting to navigate.
|
|
112
|
+
* Navigation blocker function receives a `location` to which the application (or the user) is attempting to navigate.
|
|
114
113
|
*
|
|
115
|
-
* The `location` argument is `null` when the web browser tab is about to be closed.
|
|
114
|
+
* * The `location` argument is `null` when the web browser tab is about to be closed.
|
|
115
|
+
* * The `location` argument is of type `NavigationLocation` when a `.push()` or `.replace()` action is blocked.
|
|
116
|
+
* * The `location` argument is of type `Location` when blocking a navigation that was initiated outside of the application code.
|
|
117
|
+
* For example, when the user clicks "Back" or "Forward" button in a web browser.
|
|
116
118
|
*/
|
|
117
119
|
export interface NavigationBlocker {
|
|
118
|
-
(location: Location |
|
|
120
|
+
(location: Location | NavigationLocation | null): NavigationBlockerResult;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// I dunno why did they use an `interface` here.
|
|
124
|
+
export interface BeforeLocationChangeListener {
|
|
125
|
+
(location: Location): void;
|
|
119
126
|
}
|
|
120
127
|
|
|
121
128
|
export function addBasePath<L extends InputLocation>(
|
|
@@ -131,15 +138,20 @@ export function getLocationUrl(location: InputLocationObject): string;
|
|
|
131
138
|
export function parseLocationUrl(locationUrl: string): LocationBase;
|
|
132
139
|
|
|
133
140
|
export function createMiddlewares(
|
|
134
|
-
|
|
141
|
+
session: SessionBase,
|
|
135
142
|
options?: CreateMiddlewaresOptions,
|
|
136
143
|
): Middleware[];
|
|
137
144
|
|
|
138
145
|
export function addNavigationBlocker(
|
|
139
|
-
|
|
146
|
+
session: SessionBase,
|
|
140
147
|
blocker: NavigationBlocker,
|
|
141
148
|
): () => void;
|
|
142
149
|
|
|
150
|
+
export function addBeforeLocationChangeListener(
|
|
151
|
+
session: SessionBase,
|
|
152
|
+
listener: BeforeLocationChangeListener,
|
|
153
|
+
): () => void;
|
|
154
|
+
|
|
143
155
|
export const ActionTypes: {
|
|
144
156
|
INIT: '@@navigation-stack/INIT';
|
|
145
157
|
PUSH: '@@navigation-stack/PUSH';
|
|
@@ -190,78 +202,71 @@ export const Actions: {
|
|
|
190
202
|
|
|
191
203
|
type BeforeDestroyListener = () => boolean | undefined;
|
|
192
204
|
|
|
193
|
-
|
|
205
|
+
interface SessionNavigation {
|
|
194
206
|
init(): void;
|
|
195
207
|
|
|
196
208
|
// Subscribes to changes in location,
|
|
197
209
|
// excluding ones that happened as a result of calling `.navigate()`.
|
|
198
210
|
subscribe(listener: (location: Location) => void): () => void;
|
|
199
211
|
|
|
200
|
-
navigate(location:
|
|
212
|
+
navigate(location: NavigationLocation): Location;
|
|
201
213
|
|
|
202
214
|
shift(delta: number): void;
|
|
203
|
-
|
|
204
|
-
addBeforeDestroyListener(listener: BeforeDestroyListener): void;
|
|
205
|
-
|
|
206
|
-
getState(key: string): string | null;
|
|
207
|
-
removeState(key: string): void;
|
|
208
|
-
setState(key: string, value: string): void;
|
|
209
215
|
}
|
|
210
216
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// excluding ones that happened as a result of calling `.navigate()`.
|
|
217
|
-
subscribe(listener: (location: Location) => void): () => void;
|
|
217
|
+
interface SessionDataStorage {
|
|
218
|
+
get(key: string): string | null;
|
|
219
|
+
remove(key: string): void;
|
|
220
|
+
set(key: string, value: string): void;
|
|
221
|
+
}
|
|
218
222
|
|
|
219
|
-
|
|
223
|
+
export interface Session {
|
|
224
|
+
navigation: SessionNavigation;
|
|
225
|
+
dataStorage: SessionDataStorage;
|
|
226
|
+
addBeforeDestroyListener(listener: BeforeDestroyListener): void;
|
|
220
227
|
|
|
221
|
-
|
|
228
|
+
// These're internal variables that're manually set under the hood.
|
|
229
|
+
// _beforeLocationChangeListenersList?: Array<BeforeLocationChangeListener>;
|
|
230
|
+
// _navigationBlockersList?: Array<NavigationBlocker>;
|
|
231
|
+
// _removeBeforeDestroyListener?: () => void;
|
|
232
|
+
// _navigationBlockersEvaluationStatus?: { cancelled?: boolean };
|
|
233
|
+
}
|
|
222
234
|
|
|
235
|
+
// This is just a copy-paste of the `session` interface above.
|
|
236
|
+
declare abstract class SessionBase implements Session {
|
|
237
|
+
navigation: SessionNavigation;
|
|
238
|
+
dataStorage: SessionDataStorage;
|
|
223
239
|
addBeforeDestroyListener(listener: BeforeDestroyListener): void;
|
|
224
240
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
// These're internal variables that're manually set under the hood.
|
|
242
|
+
// _beforeLocationChangeListenersList?: Array<BeforeLocationChangeListener>;
|
|
243
|
+
// _navigationBlockersList?: Array<NavigationBlocker>;
|
|
244
|
+
// _removeBeforeDestroyListener?: () => void;
|
|
245
|
+
// _navigationBlockersEvaluationStatus?: { cancelled?: boolean };
|
|
228
246
|
}
|
|
229
247
|
|
|
230
|
-
export class
|
|
248
|
+
export class BrowserSession extends SessionBase {}
|
|
231
249
|
|
|
232
|
-
export interface
|
|
233
|
-
save?: (
|
|
234
|
-
load?: () =>
|
|
250
|
+
export interface MemorySessionOptions {
|
|
251
|
+
save?: (data: string) => void;
|
|
252
|
+
load?: () => string | undefined | null;
|
|
235
253
|
}
|
|
236
254
|
|
|
237
|
-
export class
|
|
255
|
+
export class ServerSession extends SessionBase {
|
|
238
256
|
constructor(initialLocation: InputLocation);
|
|
239
257
|
}
|
|
240
258
|
|
|
241
|
-
export class
|
|
259
|
+
export class MemorySession extends SessionBase {
|
|
242
260
|
constructor(
|
|
243
261
|
initialLocation: InputLocation,
|
|
244
|
-
options?:
|
|
262
|
+
options?: MemorySessionOptions,
|
|
245
263
|
);
|
|
246
264
|
}
|
|
247
265
|
|
|
248
|
-
export interface QueryMiddlewareOptions {
|
|
249
|
-
stringify(query: InputLocationQuery): string;
|
|
250
|
-
parse(str: string): Query;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
export function createQueryMiddleware(
|
|
254
|
-
options: QueryMiddlewareOptions,
|
|
255
|
-
): Middleware;
|
|
256
|
-
|
|
257
|
-
export const queryMiddleware: Middleware;
|
|
258
|
-
|
|
259
|
-
export function createBasePathMiddleware(basePath?: string): Middleware;
|
|
260
|
-
|
|
261
266
|
export const locationReducer: Reducer<Location, Action>;
|
|
262
267
|
|
|
263
|
-
export class
|
|
264
|
-
constructor(
|
|
268
|
+
export class LocationDataStorage {
|
|
269
|
+
constructor(session: Session, options?: { namespace?: string });
|
|
265
270
|
|
|
266
271
|
get(location: Location, key: string): any;
|
|
267
272
|
set(location: Location, key: string, value: any): void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "navigation-stack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Handles navigation in a web browser",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"history",
|
|
@@ -92,8 +92,8 @@
|
|
|
92
92
|
"webpack": "^5.72.1"
|
|
93
93
|
},
|
|
94
94
|
"resolutions": {
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
-
"
|
|
95
|
+
"@definitelytyped/header-parser": "^0.0.41",
|
|
96
|
+
"@definitelytyped/typescript-versions": "^0.0.41",
|
|
97
|
+
"@definitelytyped/utils": "^0.0.41"
|
|
98
98
|
}
|
|
99
99
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import getLocationUrl from './getLocationUrl';
|
|
2
2
|
|
|
3
|
-
export default class
|
|
3
|
+
export default class LocationDataStorage {
|
|
4
4
|
constructor(environment, { namespace } = {}) {
|
|
5
5
|
this._environment = environment;
|
|
6
6
|
this._getFallbackLocationKey = getLocationUrl;
|
|
@@ -11,7 +11,7 @@ export default class LocationStateStorage {
|
|
|
11
11
|
const stateKey = this._getStateKey(location, key);
|
|
12
12
|
|
|
13
13
|
try {
|
|
14
|
-
const value = this._environment.
|
|
14
|
+
const value = this._environment.dataStorage.get(stateKey);
|
|
15
15
|
// === null is probably sufficient.
|
|
16
16
|
if (value === null) {
|
|
17
17
|
return undefined;
|
|
@@ -31,7 +31,7 @@ export default class LocationStateStorage {
|
|
|
31
31
|
|
|
32
32
|
if (value === undefined) {
|
|
33
33
|
try {
|
|
34
|
-
this._environment.
|
|
34
|
+
this._environment.dataStorage.remove(stateKey);
|
|
35
35
|
} catch (error) {
|
|
36
36
|
// No need to handle errors here.
|
|
37
37
|
}
|
|
@@ -44,7 +44,7 @@ export default class LocationStateStorage {
|
|
|
44
44
|
const valueString = JSON.stringify(value);
|
|
45
45
|
|
|
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.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
|
|
3
|
+
export function getBeforeLocationChangeListeners(session) {
|
|
4
|
+
return session._beforeLocationChangeListenersList || [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function addBeforeLocationChangeListenerToTheList(listener, session) {
|
|
8
|
+
if (!session._beforeLocationChangeListenersList) {
|
|
9
|
+
session._beforeLocationChangeListenersList = [];
|
|
10
|
+
}
|
|
11
|
+
session._beforeLocationChangeListenersList.push(listener);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function removeBeforeLocationChangeListenerFromTheList(listener, session) {
|
|
15
|
+
if (session._beforeLocationChangeListenersList) {
|
|
16
|
+
session._beforeLocationChangeListenersList =
|
|
17
|
+
session._beforeLocationChangeListenersList.filter((_) => _ !== listener);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function removeAllBeforeLocationChangeListeners(session) {
|
|
22
|
+
session._beforeLocationChangeListenersList = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Runs the `listener` while ignoring any errors that might be thrown by it.
|
|
26
|
+
function runBeforeLocationChangeListener(listener, location) {
|
|
27
|
+
try {
|
|
28
|
+
listener(location);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
// eslint-disable-next-line no-console
|
|
31
|
+
console.warn(
|
|
32
|
+
`Ignoring before location change listener \`${listener.name}\` that failed with \`${error}\`.`,
|
|
33
|
+
);
|
|
34
|
+
// eslint-disable-next-line no-console
|
|
35
|
+
console.error(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Runs all listeners in order.
|
|
40
|
+
export function runBeforeLocationChangeListeners(
|
|
41
|
+
navigationListeners,
|
|
42
|
+
toLocation,
|
|
43
|
+
) {
|
|
44
|
+
for (const listener of navigationListeners) {
|
|
45
|
+
runBeforeLocationChangeListener(listener, toLocation);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function addBeforeLocationChangeListener(session, listener) {
|
|
50
|
+
addBeforeLocationChangeListenerToTheList(listener, session);
|
|
51
|
+
return () => {
|
|
52
|
+
removeBeforeLocationChangeListenerFromTheList(listener, session);
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/createMiddlewares.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
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
7
|
|
|
7
|
-
export default function createMiddlewares(
|
|
8
|
-
// Allows temporarily ignoring
|
|
9
|
-
let
|
|
10
|
-
const
|
|
11
|
-
|
|
8
|
+
export default function createMiddlewares(session, options) {
|
|
9
|
+
// Allows temporarily ignoring location update events.
|
|
10
|
+
let shouldIgnoreLocationSubscriptionEvents = false;
|
|
11
|
+
const ignoreLocationSubscriptionEvents = (func) => {
|
|
12
|
+
shouldIgnoreLocationSubscriptionEvents = true;
|
|
12
13
|
func();
|
|
13
|
-
|
|
14
|
+
shouldIgnoreLocationSubscriptionEvents = false;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
return [
|
|
@@ -23,19 +24,22 @@ export default function createMiddlewares(environment, options) {
|
|
|
23
24
|
createBasePathMiddleware(options && options.basePath),
|
|
24
25
|
// Allows blocking navigation.
|
|
25
26
|
// Handles `NAVIGATE` actions dispatched by the application itself.
|
|
26
|
-
createNavigationBlockerMiddleware(
|
|
27
|
-
|
|
27
|
+
createNavigationBlockerMiddleware(session, {
|
|
28
|
+
ignoreLocationSubscriptionEvents,
|
|
28
29
|
}),
|
|
29
|
-
// This "middleware" performs the actual navigation according to the `
|
|
30
|
-
// For example, when `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
// This "middleware" performs the actual navigation according to the `session` being used.
|
|
31
|
+
// For example, when `BrowserSession` is used, it calls methods of the `history` object.
|
|
32
|
+
createLocationMiddleware(session, {
|
|
33
|
+
shouldIgnoreLocationSubscriptionEvents: () =>
|
|
34
|
+
shouldIgnoreLocationSubscriptionEvents,
|
|
34
35
|
}),
|
|
35
36
|
// Allows blocking navigation.
|
|
36
|
-
// Handles location `UPDATE` actions dispatched
|
|
37
|
-
createNavigationBlockerMiddleware(
|
|
38
|
-
|
|
37
|
+
// Handles location `UPDATE` actions dispatched in response to location update events.
|
|
38
|
+
createNavigationBlockerMiddleware(session, {
|
|
39
|
+
ignoreLocationSubscriptionEvents,
|
|
39
40
|
}),
|
|
41
|
+
// Allows subscribing to upcoming location changes
|
|
42
|
+
// before those changes are applied in the `location` object in the state.
|
|
43
|
+
createBeforeLocationChangeListenerMiddleware(session),
|
|
40
44
|
];
|
|
41
45
|
}
|