navigation-stack 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.github/workflows/main.yml +39 -39
  2. package/README.md +128 -27
  3. package/lib/cjs/{LocationStateStorage.js → LocationDataStorage.js} +5 -5
  4. package/lib/cjs/addBeforeLocationChangeListener.js +7 -0
  5. package/lib/cjs/beforeLocationChangeListeners.js +51 -0
  6. package/lib/cjs/createMiddlewares.js +21 -17
  7. package/lib/cjs/index.js +9 -9
  8. package/lib/cjs/middleware/createBasePathMiddleware.js +2 -2
  9. package/lib/cjs/middleware/createBeforeLocationChangeListenerMiddleware.js +39 -0
  10. package/lib/cjs/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
  11. package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +62 -29
  12. package/lib/cjs/middleware/createTransformLocationMiddleware.js +2 -2
  13. package/lib/cjs/navigationBlockers.js +55 -47
  14. package/lib/cjs/normalizeInputLocation.js +1 -0
  15. package/lib/cjs/parseLocationUrl.js +2 -0
  16. package/lib/cjs/parseQueryFromSearch.js +1 -1
  17. package/lib/cjs/session/BrowserSession.js +229 -0
  18. package/lib/cjs/session/MemorySession.js +223 -0
  19. package/lib/cjs/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -16
  20. package/lib/esm/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
  21. package/lib/esm/addBeforeLocationChangeListener.js +2 -0
  22. package/lib/esm/beforeLocationChangeListeners.js +44 -0
  23. package/lib/esm/createMiddlewares.js +21 -17
  24. package/lib/esm/index.js +4 -4
  25. package/lib/esm/middleware/createBasePathMiddleware.js +2 -2
  26. package/lib/esm/middleware/createBeforeLocationChangeListenerMiddleware.js +34 -0
  27. package/lib/esm/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +11 -13
  28. package/lib/esm/middleware/createNavigationBlockerMiddleware.js +62 -29
  29. package/lib/esm/middleware/createTransformLocationMiddleware.js +2 -2
  30. package/lib/esm/navigationBlockers.js +55 -47
  31. package/lib/esm/normalizeInputLocation.js +1 -0
  32. package/lib/esm/parseLocationUrl.js +2 -0
  33. package/lib/esm/parseQueryFromSearch.js +1 -1
  34. package/lib/esm/session/BrowserSession.js +223 -0
  35. package/lib/esm/session/MemorySession.js +217 -0
  36. package/lib/esm/{environment/ServerEnvironment.js → session/ServerSession.js} +27 -15
  37. package/lib/index.d.ts +64 -59
  38. package/package.json +4 -4
  39. package/src/{LocationStateStorage.js → LocationDataStorage.js} +4 -4
  40. package/src/addBeforeLocationChangeListener.js +2 -0
  41. package/src/beforeLocationChangeListeners.js +54 -0
  42. package/src/createMiddlewares.js +21 -17
  43. package/src/index.js +4 -4
  44. package/src/middleware/createBasePathMiddleware.js +2 -2
  45. package/src/middleware/createBeforeLocationChangeListenerMiddleware.js +40 -0
  46. package/src/middleware/{createEnvironmentMiddleware.js → createLocationMiddleware.js} +12 -14
  47. package/src/middleware/createNavigationBlockerMiddleware.js +68 -28
  48. package/src/middleware/createTransformLocationMiddleware.js +2 -2
  49. package/src/navigationBlockers.js +68 -49
  50. package/src/normalizeInputLocation.js +1 -0
  51. package/src/parseLocationUrl.js +2 -0
  52. package/src/parseQueryFromSearch.js +1 -1
  53. package/src/session/BrowserSession.js +225 -0
  54. package/src/session/MemorySession.js +219 -0
  55. package/src/{environment/ServerEnvironment.js → session/ServerSession.js} +28 -15
  56. package/test/{LocationStateStorage.test.js → LocationDataStorage.test.js} +6 -6
  57. package/test/createMiddlewares.test.js +2 -2
  58. package/test/helpers.js +1 -1
  59. package/test/index.test.js +3 -3
  60. package/test/middleware/createBasePathMiddleware.test.js +7 -7
  61. package/test/middleware/createBeforeLocationChangeListenerMiddleware.test.js +141 -0
  62. package/test/middleware/createNavigationBlockerMiddleware.test.js +96 -97
  63. package/test/middleware/createTransformLocationMiddleware.test.js +1 -1
  64. package/test/normalizeInputLocation.test.js +3 -0
  65. package/test/parseLocationUrl.test.js +2 -0
  66. package/test/{environment/BrowserEnvironment.test.js → session/BrowserSession.test.js} +35 -18
  67. package/test/session/MemorySession.test.js +244 -0
  68. package/test/session/ServerSession.test.js +23 -0
  69. package/types/index.d.ts +64 -59
  70. package/lib/cjs/environment/BrowserEnvironment.js +0 -111
  71. package/lib/cjs/environment/MemoryEnvironment.js +0 -150
  72. package/lib/esm/environment/BrowserEnvironment.js +0 -104
  73. package/lib/esm/environment/MemoryEnvironment.js +0 -143
  74. package/src/environment/BrowserEnvironment.js +0 -109
  75. package/src/environment/MemoryEnvironment.js +0 -151
  76. package/test/environment/MemoryEnvironment.test.js +0 -218
  77. package/test/environment/ServerEnvironment.test.js +0 -23
@@ -1,151 +0,0 @@
1
- import normalizeInputLocation from '../normalizeInputLocation';
2
-
3
- export default class MemoryEnvironment {
4
- constructor(initialLocation, { save, load } = {}) {
5
- this._save = save;
6
- this._load = load;
7
-
8
- const initialState = load ? this._loadState() : null;
9
- if (initialState) {
10
- this._stack = initialState.stack;
11
- this._index = initialState.index;
12
- this._state = initialState.state;
13
- } else {
14
- this._stack = [normalizeInputLocation(initialLocation)];
15
- this._index = 0;
16
- this._state = {};
17
- }
18
-
19
- this._keyPrefix = Math.random().toString(36).slice(2, 8);
20
- this._keyIndex = 0;
21
-
22
- this._listener = null;
23
- }
24
-
25
- _loadState() {
26
- try {
27
- const { stack, index, state } = JSON.parse(this._load());
28
-
29
- // Check that the stack and index at least seem reasonable before using
30
- // them as state. This isn't foolproof, but it might prevent mistakes.
31
- // Also perform a basic validation of `state`.
32
- if (
33
- Array.isArray(stack) &&
34
- typeof index === 'number' &&
35
- stack[index] &&
36
- typeof state === 'object' &&
37
- state !== null
38
- ) {
39
- return { stack, index, state };
40
- }
41
- } catch (error) {} // eslint-disable-line no-empty
42
-
43
- return null;
44
- }
45
-
46
- init(delta = 0) {
47
- return {
48
- ...this._stack[this._index],
49
- action: 'POP',
50
- index: this._index,
51
- delta,
52
- };
53
- }
54
-
55
- subscribe(listener) {
56
- this._listener = listener;
57
-
58
- return () => {
59
- this._listener = null;
60
- };
61
- }
62
-
63
- navigate(location) {
64
- const { action, pathname, search, query, hash, state } = location;
65
-
66
- const push = action === 'PUSH';
67
-
68
- if (!push && action !== 'REPLACE')
69
- throw Error(`Unrecognized browser environment action: ${action}`);
70
-
71
- const delta = push ? 1 : 0;
72
- this._index += delta;
73
-
74
- const keyIndex = this._keyIndex++;
75
- const key = `${this._keyPrefix}:${keyIndex.toString(36)}`;
76
-
77
- this._stack[this._index] = { pathname, search, query, hash, state, key };
78
- if (push) {
79
- this._stack.length = this._index + 1;
80
- }
81
-
82
- if (this._save) {
83
- this._saveState();
84
- }
85
-
86
- return { ...location, key, index: this._index, delta };
87
- }
88
-
89
- shift(delta) {
90
- const prevIndex = this._index;
91
-
92
- this._index = Math.min(
93
- Math.max(this._index + delta, 0),
94
- this._stack.length - 1,
95
- );
96
-
97
- if (this._index === prevIndex) {
98
- return;
99
- }
100
-
101
- if (this._save) {
102
- this._saveState();
103
- }
104
-
105
- if (this._listener) {
106
- this._listener(this.init(this._index - prevIndex));
107
- }
108
- }
109
-
110
- // "Before destroy" listeners are currently ignored.
111
- // If required, one could implement a `_destroy()` method
112
- // and there check that the listeners actually do get called.
113
- addBeforeDestroyListener(listener) {
114
- return () => {
115
- this._removeBeforeDestroyListener(listener);
116
- };
117
- }
118
-
119
- // This method is used in tests.
120
- _removeBeforeDestroyListener() {}
121
-
122
- _saveState() {
123
- try {
124
- this._save(
125
- JSON.stringify({
126
- stack: this._stack,
127
- index: this._index,
128
- state: this._state,
129
- }),
130
- );
131
- } catch (error) {} // eslint-disable-line no-empty
132
- }
133
-
134
- // Returns either a `string` value or `null` if the key doesn't exist.
135
- getState(key) {
136
- if (key in this._state) {
137
- return this._state[key];
138
- }
139
- return null;
140
- }
141
-
142
- removeState(key) {
143
- if (key in this._state) {
144
- delete this._state[key];
145
- }
146
- }
147
-
148
- setState(key, value) {
149
- this._state[key] = value;
150
- }
151
- }
@@ -1,218 +0,0 @@
1
- import MemoryEnvironment from '../../src/environment/MemoryEnvironment';
2
-
3
- const STATE_KEY = '@@navigation-stack/environment-state';
4
-
5
- function save(state) {
6
- window.sessionStorage.setItem(STATE_KEY, state);
7
- }
8
-
9
- function load() {
10
- return window.sessionStorage.getItem(STATE_KEY);
11
- }
12
-
13
- describe('MemoryEnvironment', () => {
14
- it('should parse the initial location', () => {
15
- const environment = new MemoryEnvironment('/foo?bar=baz#qux');
16
-
17
- expect(environment.init()).to.eql({
18
- action: 'POP',
19
- pathname: '/foo',
20
- search: '?bar=baz',
21
- query: {
22
- bar: 'baz',
23
- },
24
- hash: '#qux',
25
- index: 0,
26
- delta: 0,
27
- });
28
- });
29
-
30
- it('should support basic navigation', () => {
31
- const environment = new MemoryEnvironment('/foo');
32
-
33
- const listener = sinon.spy();
34
- environment.subscribe(listener);
35
-
36
- const barLocation = environment.navigate({
37
- action: 'PUSH',
38
- pathname: '/bar',
39
- state: { the: 'state' },
40
- });
41
-
42
- expect(barLocation).to.deep.include({
43
- action: 'PUSH',
44
- pathname: '/bar',
45
- index: 1,
46
- delta: 1,
47
- state: { the: 'state' },
48
- });
49
- expect(barLocation.key).not.to.be.empty();
50
-
51
- expect(
52
- environment.navigate({ action: 'PUSH', pathname: '/baz' }),
53
- ).to.include({
54
- action: 'PUSH',
55
- pathname: '/baz',
56
- index: 2,
57
- delta: 1,
58
- });
59
-
60
- expect(
61
- environment.navigate({ action: 'REPLACE', pathname: '/qux' }),
62
- ).to.include({
63
- action: 'REPLACE',
64
- pathname: '/qux',
65
- index: 2,
66
- delta: 0,
67
- });
68
-
69
- expect(listener).not.to.have.been.called();
70
-
71
- environment.shift(-1);
72
-
73
- expect(listener).to.have.been.calledOnce();
74
- expect(listener.firstCall.args[0]).to.deep.include({
75
- action: 'POP',
76
- pathname: '/bar',
77
- key: barLocation.key,
78
- index: 1,
79
- delta: -1,
80
- state: { the: 'state' },
81
- });
82
- });
83
-
84
- it('should support subscribing and unsubscribing', () => {
85
- const environment = new MemoryEnvironment('/foo');
86
- environment.navigate({ action: 'PUSH', pathname: '/bar' });
87
- environment.navigate({ action: 'PUSH', pathname: '/baz' });
88
-
89
- const listener = sinon.spy();
90
- const unsubscribe = environment.subscribe(listener);
91
-
92
- environment.shift(-1);
93
-
94
- expect(listener).to.have.been.calledOnce();
95
- expect(listener.firstCall.args[0]).to.include({
96
- action: 'POP',
97
- pathname: '/bar',
98
- });
99
- listener.resetHistory();
100
-
101
- unsubscribe();
102
-
103
- environment.shift(-1);
104
-
105
- expect(listener).not.to.have.been.called();
106
- });
107
-
108
- it('should respect stack bounds', () => {
109
- const environment = new MemoryEnvironment('/foo');
110
- environment.navigate({ action: 'PUSH', pathname: '/bar' });
111
- environment.navigate({ action: 'PUSH', pathname: '/baz' });
112
-
113
- const listener = sinon.spy();
114
- environment.subscribe(listener);
115
-
116
- environment.shift(-390);
117
-
118
- expect(listener).to.have.been.calledOnce();
119
- expect(listener.firstCall.args[0]).to.include({
120
- action: 'POP',
121
- pathname: '/foo',
122
- delta: -2,
123
- });
124
- listener.resetHistory();
125
-
126
- environment.shift(-1);
127
-
128
- expect(listener).not.to.have.been.called();
129
-
130
- environment.shift(+22);
131
-
132
- expect(listener).to.have.been.calledOnce();
133
- expect(listener.firstCall.args[0]).to.include({
134
- action: 'POP',
135
- pathname: '/baz',
136
- delta: 2,
137
- });
138
- listener.resetHistory();
139
-
140
- environment.shift(+1);
141
-
142
- expect(listener).not.to.have.been.called();
143
- });
144
-
145
- it('should reset forward entries on push', () => {
146
- const environment = new MemoryEnvironment('/foo');
147
- environment.navigate({ action: 'PUSH', pathname: '/bar' });
148
- environment.navigate({ action: 'PUSH', pathname: '/baz' });
149
- environment.shift(-2);
150
- environment.navigate({ action: 'REPLACE', pathname: '/qux' });
151
-
152
- const listener = sinon.spy();
153
- environment.subscribe(listener);
154
-
155
- environment.shift(+1);
156
-
157
- expect(listener).to.have.been.calledOnce();
158
- expect(listener.firstCall.args[0]).to.include({
159
- action: 'POP',
160
- pathname: '/bar',
161
- delta: 1,
162
- });
163
- });
164
-
165
- it('should not reset forward entries on replace', () => {
166
- const environment = new MemoryEnvironment('/foo');
167
- environment.navigate({ action: 'PUSH', pathname: '/bar' });
168
- environment.navigate({ action: 'PUSH', pathname: '/baz' });
169
- environment.shift(-2);
170
- environment.navigate({ action: 'PUSH', pathname: '/qux' });
171
-
172
- const listener = sinon.spy();
173
- environment.subscribe(listener);
174
-
175
- environment.shift(+1);
176
-
177
- expect(listener).not.to.have.been.called();
178
- });
179
-
180
- describe('persistence', () => {
181
- beforeEach(() => {
182
- window.sessionStorage.clear();
183
- });
184
-
185
- it('should support persistence', () => {
186
- const environment1 = new MemoryEnvironment('/foo', { save, load });
187
- expect(environment1.init()).to.include({
188
- pathname: '/foo',
189
- });
190
-
191
- environment1.navigate({ action: 'PUSH', pathname: '/bar' });
192
- environment1.navigate({ action: 'PUSH', pathname: '/baz' });
193
- environment1.shift(-1);
194
-
195
- const environment2 = new MemoryEnvironment('/foo', { save, load });
196
- expect(environment2.init()).to.include({
197
- pathname: '/bar',
198
- });
199
-
200
- environment2.shift(+1);
201
- expect(environment2.init()).to.include({
202
- pathname: '/baz',
203
- });
204
- });
205
-
206
- it('should ignore broken session storage entry', () => {
207
- sessionStorage.setItem(
208
- '@@navigation-stack/state',
209
- JSON.stringify({ stack: [], index: 2 }),
210
- );
211
-
212
- const environment = new MemoryEnvironment('/foo', { save, load });
213
- expect(environment.init()).to.include({
214
- pathname: '/foo',
215
- });
216
- });
217
- });
218
- });
@@ -1,23 +0,0 @@
1
- import ServerEnvironment from '../../src/environment/ServerEnvironment';
2
-
3
- describe('ServerEnvironment', () => {
4
- it('should parse the initial location', () => {
5
- const environment = new ServerEnvironment('/foo?bar=baz#qux');
6
-
7
- expect(environment.init()).to.eql({
8
- action: 'POP',
9
- pathname: '/foo',
10
- search: '?bar=baz',
11
- query: {
12
- bar: 'baz',
13
- },
14
- hash: '#qux',
15
- });
16
- });
17
-
18
- it('should have dummy support for subscriptions', () => {
19
- const environment = new ServerEnvironment('/foo?bar=baz#qux');
20
- const unsubscribe = environment.subscribe();
21
- expect(unsubscribe).to.not.throw();
22
- });
23
- });