zero-query 0.9.8 → 1.0.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 (99) hide show
  1. package/README.md +55 -31
  2. package/cli/args.js +1 -1
  3. package/cli/commands/build.js +2 -2
  4. package/cli/commands/bundle.js +15 -15
  5. package/cli/commands/create.js +41 -7
  6. package/cli/commands/dev/devtools/index.js +1 -1
  7. package/cli/commands/dev/devtools/js/core.js +14 -14
  8. package/cli/commands/dev/devtools/js/elements.js +4 -4
  9. package/cli/commands/dev/devtools/js/stats.js +1 -1
  10. package/cli/commands/dev/devtools/styles.css +2 -2
  11. package/cli/commands/dev/index.js +2 -2
  12. package/cli/commands/dev/logger.js +1 -1
  13. package/cli/commands/dev/overlay.js +21 -14
  14. package/cli/commands/dev/server.js +5 -5
  15. package/cli/commands/dev/validator.js +7 -7
  16. package/cli/commands/dev/watcher.js +6 -6
  17. package/cli/help.js +4 -2
  18. package/cli/index.js +2 -2
  19. package/cli/scaffold/default/app/app.js +17 -18
  20. package/cli/scaffold/default/app/components/about.js +9 -9
  21. package/cli/scaffold/default/app/components/api-demo.js +6 -6
  22. package/cli/scaffold/default/app/components/contact-card.js +4 -4
  23. package/cli/scaffold/default/app/components/contacts/contacts.css +2 -2
  24. package/cli/scaffold/default/app/components/contacts/contacts.html +3 -3
  25. package/cli/scaffold/default/app/components/contacts/contacts.js +11 -11
  26. package/cli/scaffold/default/app/components/counter.js +8 -8
  27. package/cli/scaffold/default/app/components/home.js +13 -13
  28. package/cli/scaffold/default/app/components/not-found.js +1 -1
  29. package/cli/scaffold/default/app/components/playground/playground.css +1 -1
  30. package/cli/scaffold/default/app/components/playground/playground.html +11 -11
  31. package/cli/scaffold/default/app/components/playground/playground.js +11 -11
  32. package/cli/scaffold/default/app/components/todos.js +8 -8
  33. package/cli/scaffold/default/app/components/toolkit/toolkit.css +1 -1
  34. package/cli/scaffold/default/app/components/toolkit/toolkit.html +4 -4
  35. package/cli/scaffold/default/app/components/toolkit/toolkit.js +7 -7
  36. package/cli/scaffold/default/app/routes.js +1 -1
  37. package/cli/scaffold/default/app/store.js +1 -1
  38. package/cli/scaffold/default/global.css +2 -2
  39. package/cli/scaffold/default/index.html +2 -2
  40. package/cli/scaffold/minimal/app/app.js +6 -7
  41. package/cli/scaffold/minimal/app/components/about.js +5 -5
  42. package/cli/scaffold/minimal/app/components/counter.js +6 -6
  43. package/cli/scaffold/minimal/app/components/home.js +8 -8
  44. package/cli/scaffold/minimal/app/components/not-found.js +1 -1
  45. package/cli/scaffold/minimal/app/routes.js +1 -1
  46. package/cli/scaffold/minimal/app/store.js +1 -1
  47. package/cli/scaffold/minimal/global.css +2 -2
  48. package/cli/scaffold/minimal/index.html +1 -1
  49. package/cli/scaffold/ssr/app/app.js +29 -0
  50. package/cli/scaffold/ssr/app/components/about.js +28 -0
  51. package/cli/scaffold/ssr/app/components/home.js +37 -0
  52. package/cli/scaffold/ssr/app/components/not-found.js +15 -0
  53. package/cli/scaffold/ssr/app/routes.js +6 -0
  54. package/cli/scaffold/ssr/global.css +113 -0
  55. package/cli/scaffold/ssr/index.html +31 -0
  56. package/cli/scaffold/ssr/package.json +8 -0
  57. package/cli/scaffold/ssr/server/index.js +118 -0
  58. package/cli/utils.js +6 -6
  59. package/dist/zquery.dist.zip +0 -0
  60. package/dist/zquery.js +565 -228
  61. package/dist/zquery.min.js +2 -2
  62. package/index.d.ts +25 -12
  63. package/index.js +11 -7
  64. package/package.json +9 -3
  65. package/src/component.js +64 -63
  66. package/src/core.js +15 -15
  67. package/src/diff.js +38 -38
  68. package/src/errors.js +72 -18
  69. package/src/expression.js +15 -17
  70. package/src/http.js +4 -4
  71. package/src/package.json +1 -0
  72. package/src/reactive.js +75 -9
  73. package/src/router.js +104 -24
  74. package/src/ssr.js +133 -39
  75. package/src/store.js +103 -21
  76. package/src/utils.js +64 -12
  77. package/tests/audit.test.js +143 -15
  78. package/tests/cli.test.js +20 -20
  79. package/tests/component.test.js +121 -121
  80. package/tests/core.test.js +56 -56
  81. package/tests/diff.test.js +42 -42
  82. package/tests/errors.test.js +425 -147
  83. package/tests/expression.test.js +58 -53
  84. package/tests/http.test.js +20 -20
  85. package/tests/reactive.test.js +185 -24
  86. package/tests/router.test.js +501 -74
  87. package/tests/ssr.test.js +444 -10
  88. package/tests/store.test.js +264 -23
  89. package/tests/utils.test.js +163 -26
  90. package/types/collection.d.ts +2 -2
  91. package/types/component.d.ts +5 -5
  92. package/types/errors.d.ts +36 -4
  93. package/types/http.d.ts +3 -3
  94. package/types/misc.d.ts +9 -9
  95. package/types/reactive.d.ts +25 -3
  96. package/types/router.d.ts +10 -6
  97. package/types/ssr.d.ts +22 -2
  98. package/types/store.d.ts +40 -5
  99. package/types/utils.d.ts +1 -1
package/src/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery HTTP Lightweight fetch wrapper
2
+ * zQuery HTTP - Lightweight fetch wrapper
3
3
  *
4
4
  * Clean API for GET/POST/PUT/PATCH/DELETE with:
5
5
  * - Auto JSON serialization/deserialization
@@ -189,7 +189,7 @@ export const http = {
189
189
 
190
190
  /**
191
191
  * Add request interceptor
192
- * @param {Function} fn (fetchOpts, url) → void | false | { url, options }
192
+ * @param {Function} fn - (fetchOpts, url) → void | false | { url, options }
193
193
  * @returns {Function} unsubscribe function
194
194
  */
195
195
  onRequest(fn) {
@@ -202,7 +202,7 @@ export const http = {
202
202
 
203
203
  /**
204
204
  * Add response interceptor
205
- * @param {Function} fn (result) → void
205
+ * @param {Function} fn - (result) → void
206
206
  * @returns {Function} unsubscribe function
207
207
  */
208
208
  onResponse(fn) {
@@ -214,7 +214,7 @@ export const http = {
214
214
  },
215
215
 
216
216
  /**
217
- * Clear interceptors all, or just 'request' / 'response'
217
+ * Clear interceptors - all, or just 'request' / 'response'
218
218
  */
219
219
  clearInterceptors(type) {
220
220
  if (!type || type === 'request') _interceptors.request.length = 0;
@@ -0,0 +1 @@
1
+ { "type": "module" }
package/src/reactive.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * zQuery Reactive Proxy-based deep reactivity system
2
+ * zQuery Reactive - Proxy-based deep reactivity system
3
3
  *
4
4
  * Creates observable objects that trigger callbacks on mutation.
5
5
  * Used internally by components and store for auto-updates.
@@ -67,7 +67,7 @@ export function reactive(target, onChange, _path = '') {
67
67
 
68
68
 
69
69
  // ---------------------------------------------------------------------------
70
- // Signal lightweight reactive primitive (inspired by Solid/Preact signals)
70
+ // Signal - lightweight reactive primitive (inspired by Solid/Preact signals)
71
71
  // ---------------------------------------------------------------------------
72
72
  export class Signal {
73
73
  constructor(value) {
@@ -96,7 +96,11 @@ export class Signal {
96
96
  peek() { return this._value; }
97
97
 
98
98
  _notify() {
99
- // Snapshot subscribers before iterating — a subscriber might modify
99
+ if (Signal._batching) {
100
+ Signal._batchQueue.add(this);
101
+ return;
102
+ }
103
+ // Snapshot subscribers before iterating - a subscriber might modify
100
104
  // the set (e.g., an effect re-running, adding itself back)
101
105
  const subs = [...this._subscribers];
102
106
  for (let i = 0; i < subs.length; i++) {
@@ -117,10 +121,13 @@ export class Signal {
117
121
 
118
122
  // Active effect tracking
119
123
  Signal._activeEffect = null;
124
+ // Batch state
125
+ Signal._batching = false;
126
+ Signal._batchQueue = new Set();
120
127
 
121
128
  /**
122
129
  * Create a signal
123
- * @param {*} initial initial value
130
+ * @param {*} initial - initial value
124
131
  * @returns {Signal}
125
132
  */
126
133
  export function signal(initial) {
@@ -129,7 +136,7 @@ export function signal(initial) {
129
136
 
130
137
  /**
131
138
  * Create a computed signal (derived from other signals)
132
- * @param {Function} fn computation function
139
+ * @param {Function} fn - computation function
133
140
  * @returns {Signal}
134
141
  */
135
142
  export function computed(fn) {
@@ -147,10 +154,10 @@ export function computed(fn) {
147
154
  /**
148
155
  * Create a side-effect that auto-tracks signal dependencies.
149
156
  * Returns a dispose function that removes the effect from all
150
- * signals it subscribed to prevents memory leaks.
157
+ * signals it subscribed to - prevents memory leaks.
151
158
  *
152
- * @param {Function} fn effect function
153
- * @returns {Function} dispose function
159
+ * @param {Function} fn - effect function
160
+ * @returns {Function} - dispose function
154
161
  */
155
162
  export function effect(fn) {
156
163
  const execute = () => {
@@ -183,6 +190,65 @@ export function effect(fn) {
183
190
  }
184
191
  execute._deps.clear();
185
192
  }
186
- // Don't clobber _activeEffect another effect may be running
193
+ // Don't clobber _activeEffect - another effect may be running
187
194
  };
188
195
  }
196
+
197
+
198
+ // ---------------------------------------------------------------------------
199
+ // batch() - defer signal notifications until the batch completes
200
+ // ---------------------------------------------------------------------------
201
+
202
+ /**
203
+ * Batch multiple signal writes - subscribers and effects fire once at the end.
204
+ * @param {Function} fn - function that performs signal writes
205
+ */
206
+ export function batch(fn) {
207
+ if (Signal._batching) {
208
+ // Already inside a batch, just run
209
+ fn();
210
+ return;
211
+ }
212
+ Signal._batching = true;
213
+ Signal._batchQueue.clear();
214
+ try {
215
+ fn();
216
+ } finally {
217
+ Signal._batching = false;
218
+ // Collect all unique subscribers across all queued signals
219
+ // so each subscriber/effect runs exactly once
220
+ const subs = new Set();
221
+ for (const sig of Signal._batchQueue) {
222
+ for (const sub of sig._subscribers) {
223
+ subs.add(sub);
224
+ }
225
+ }
226
+ Signal._batchQueue.clear();
227
+ for (const sub of subs) {
228
+ try { sub(); }
229
+ catch (err) {
230
+ reportError(ErrorCode.SIGNAL_CALLBACK, 'Signal subscriber threw', {}, err);
231
+ }
232
+ }
233
+ }
234
+ }
235
+
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // untracked() - read signals without creating dependencies
239
+ // ---------------------------------------------------------------------------
240
+
241
+ /**
242
+ * Execute a function without tracking signal reads as dependencies.
243
+ * @param {Function} fn - function to run
244
+ * @returns {*} the return value of fn
245
+ */
246
+ export function untracked(fn) {
247
+ const prev = Signal._activeEffect;
248
+ Signal._activeEffect = null;
249
+ try {
250
+ return fn();
251
+ } finally {
252
+ Signal._activeEffect = prev;
253
+ }
254
+ }
package/src/router.js CHANGED
@@ -1,14 +1,13 @@
1
1
  /**
2
- * zQuery Router Client-side SPA router
2
+ * zQuery Router - Client-side SPA router
3
3
  *
4
4
  * Supports hash mode (#/path) and history mode (/path).
5
5
  * Route params, query strings, navigation guards, and lazy loading.
6
6
  * Sub-route history substates for in-page UI changes (modals, tabs, etc.).
7
7
  *
8
8
  * Usage:
9
+ * // HTML: <z-outlet></z-outlet>
9
10
  * $.router({
10
- * el: '#app',
11
- * mode: 'hash',
12
11
  * routes: [
13
12
  * { path: '/', component: 'home-page' },
14
13
  * { path: '/user/:id', component: 'user-profile' },
@@ -44,7 +43,7 @@ function _shallowEqual(a, b) {
44
43
  class Router {
45
44
  constructor(config = {}) {
46
45
  this._el = null;
47
- // file:// protocol can't use pushState always force hash mode
46
+ // file:// protocol can't use pushState - always force hash mode
48
47
  const isFile = typeof location !== 'undefined' && location.protocol === 'file:';
49
48
  this._mode = isFile ? 'hash' : (config.mode || 'history');
50
49
 
@@ -79,8 +78,30 @@ class Router {
79
78
  this._inSubstate = false; // true while substate entries are in the history stack
80
79
 
81
80
  // Set outlet element
81
+ // Priority: explicit config.el → <z-outlet> tag in the DOM
82
82
  if (config.el) {
83
83
  this._el = typeof config.el === 'string' ? document.querySelector(config.el) : config.el;
84
+ } else if (typeof document !== 'undefined') {
85
+ const outlet = document.querySelector('z-outlet');
86
+ if (outlet) {
87
+ this._el = outlet;
88
+ // Read inline attribute overrides from <z-outlet> (config takes priority)
89
+ if (!config.fallback && outlet.getAttribute('fallback')) {
90
+ this._fallback = outlet.getAttribute('fallback');
91
+ }
92
+ if (!config.mode && outlet.getAttribute('mode')) {
93
+ const attrMode = outlet.getAttribute('mode');
94
+ if (attrMode === 'hash' || attrMode === 'history') {
95
+ this._mode = isFile ? 'hash' : attrMode;
96
+ }
97
+ }
98
+ if (config.base == null && outlet.getAttribute('base')) {
99
+ let ob = outlet.getAttribute('base');
100
+ ob = String(ob).replace(/\/+$/, '');
101
+ if (ob && !ob.startsWith('/')) ob = '/' + ob;
102
+ this._base = ob;
103
+ }
104
+ }
84
105
  }
85
106
 
86
107
  // Register routes
@@ -88,21 +109,43 @@ class Router {
88
109
  config.routes.forEach(r => this.add(r));
89
110
  }
90
111
 
91
- // Listen for navigation store handler references for cleanup in destroy()
112
+ // Listen for navigation - store handler references for cleanup in destroy()
92
113
  if (this._mode === 'hash') {
93
114
  this._onNavEvent = () => this._resolve();
94
115
  window.addEventListener('hashchange', this._onNavEvent);
116
+ // Hash mode also needs popstate for substates (pushSubstate uses pushState)
117
+ this._onPopState = (e) => {
118
+ const st = e.state;
119
+ if (st && st[_ZQ_STATE_KEY] === 'substate') {
120
+ const handled = this._fireSubstate(st.key, st.data, 'pop');
121
+ if (handled) return;
122
+ this._resolve().then(() => {
123
+ this._fireSubstate(st.key, st.data, 'pop');
124
+ });
125
+ return;
126
+ } else if (this._inSubstate) {
127
+ this._inSubstate = false;
128
+ this._fireSubstate(null, null, 'reset');
129
+ }
130
+ };
131
+ window.addEventListener('popstate', this._onPopState);
95
132
  } else {
96
133
  this._onNavEvent = (e) => {
97
- // Check for substate pop first if a listener handles it, don't route
134
+ // Check for substate pop first - if a listener handles it, don't route
98
135
  const st = e.state;
99
136
  if (st && st[_ZQ_STATE_KEY] === 'substate') {
100
137
  const handled = this._fireSubstate(st.key, st.data, 'pop');
101
138
  if (handled) return;
102
- // Unhandled substate — fall through to route resolve
103
- // _inSubstate stays true so the next non-substate pop triggers reset
139
+ // Unhandled substate — the owning component was likely destroyed
140
+ // (e.g. user navigated away then pressed back). Resolve the route
141
+ // first (which may mount a fresh component that registers a listener),
142
+ // then retry the substate so the new listener can restore the UI.
143
+ this._resolve().then(() => {
144
+ this._fireSubstate(st.key, st.data, 'pop');
145
+ });
146
+ return;
104
147
  } else if (this._inSubstate) {
105
- // Popped past all substates notify listeners to reset to defaults
148
+ // Popped past all substates - notify listeners to reset to defaults
106
149
  this._inSubstate = false;
107
150
  this._fireSubstate(null, null, 'reset');
108
151
  }
@@ -120,13 +163,17 @@ class Router {
120
163
  if (link.getAttribute('target') === '_blank') return;
121
164
  e.preventDefault();
122
165
  let href = link.getAttribute('z-link');
166
+ // Reject absolute URLs and dangerous protocols — z-link is for internal routes only
167
+ if (href && /^[a-z][a-z0-9+.-]*:/i.test(href)) return;
123
168
  // Support z-link-params for dynamic :param interpolation
124
169
  const paramsAttr = link.getAttribute('z-link-params');
125
170
  if (paramsAttr) {
126
171
  try {
127
172
  const params = JSON.parse(paramsAttr);
128
173
  href = this._interpolateParams(href, params);
129
- } catch { /* ignore malformed JSON */ }
174
+ } catch (err) {
175
+ reportError(ErrorCode.ROUTER_RESOLVE, 'Malformed JSON in z-link-params', { href, paramsAttr }, err);
176
+ }
130
177
  }
131
178
  this.navigate(href);
132
179
  // z-to-top modifier: scroll to top after navigation
@@ -180,8 +227,8 @@ class Router {
180
227
 
181
228
  /**
182
229
  * Interpolate :param placeholders in a path with the given values.
183
- * @param {string} path e.g. '/user/:id/posts/:pid'
184
- * @param {Object} params e.g. { id: 42, pid: 7 }
230
+ * @param {string} path - e.g. '/user/:id/posts/:pid'
231
+ * @param {Object} params - e.g. { id: 42, pid: 7 }
185
232
  * @returns {string}
186
233
  */
187
234
  _interpolateParams(path, params) {
@@ -225,7 +272,7 @@ class Router {
225
272
  const currentURL = (window.location.pathname || '/') + (window.location.hash || '');
226
273
 
227
274
  if (targetURL === currentURL && !options.force) {
228
- // Same full URL (path + hash) don't push duplicate entry.
275
+ // Same full URL (path + hash) - don't push duplicate entry.
229
276
  // If only the hash changed to a fragment target, scroll to it.
230
277
  if (fragment) {
231
278
  const el = document.getElementById(fragment);
@@ -234,7 +281,7 @@ class Router {
234
281
  return this;
235
282
  }
236
283
 
237
- // Same route path but different hash fragment use replaceState
284
+ // Same route path but different hash fragment - use replaceState
238
285
  // so back goes to the previous *route*, not the previous scroll position.
239
286
  const targetPathOnly = this._base + normalized;
240
287
  const currentPathOnly = window.location.pathname || '/';
@@ -249,7 +296,7 @@ class Router {
249
296
  const el = document.getElementById(fragment);
250
297
  if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
251
298
  }
252
- // Don't re-resolve same route, just a hash change
299
+ // Don't re-resolve - same route, just a hash change
253
300
  return this;
254
301
  }
255
302
 
@@ -285,8 +332,8 @@ class Router {
285
332
 
286
333
  /**
287
334
  * Normalize an app-relative path and guard against double base-prefixing.
288
- * @param {string} path e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
289
- * @returns {string} always starts with '/'
335
+ * @param {string} path - e.g. '/docs', 'docs', or '/app/docs' when base is '/app'
336
+ * @returns {string} - always starts with '/'
290
337
  */
291
338
  _normalizePath(path) {
292
339
  let p = path && path.startsWith('/') ? path : (path ? `/${path}` : '/');
@@ -336,12 +383,12 @@ class Router {
336
383
 
337
384
  /**
338
385
  * Push a lightweight history entry for in-component UI state.
339
- * The URL path does NOT change only a history entry is added so the
386
+ * The URL path does NOT change - only a history entry is added so the
340
387
  * back button can undo the UI change (close modal, revert tab, etc.)
341
388
  * before navigating away.
342
389
  *
343
- * @param {string} key identifier (e.g. 'modal', 'tab', 'panel')
344
- * @param {*} data arbitrary state (serializable)
390
+ * @param {string} key - identifier (e.g. 'modal', 'tab', 'panel')
391
+ * @param {*} data - arbitrary state (serializable)
345
392
  * @returns {Router}
346
393
  *
347
394
  * @example
@@ -352,7 +399,7 @@ class Router {
352
399
  pushSubstate(key, data) {
353
400
  this._inSubstate = true;
354
401
  if (this._mode === 'hash') {
355
- // Hash mode: stash the substate in a global hashchange will check.
402
+ // Hash mode: stash the substate in a global - hashchange will check.
356
403
  // We still push a history entry via a sentinel hash suffix.
357
404
  const current = window.location.hash || '#/';
358
405
  window.history.pushState(
@@ -468,12 +515,12 @@ class Router {
468
515
  async __resolve() {
469
516
  // Check if we're landing on a substate entry (e.g. page refresh on a
470
517
  // substate bookmark, or hash-mode popstate). Fire listeners and bail
471
- // if handled the URL hasn't changed so there's no route to resolve.
518
+ // if handled - the URL hasn't changed so there's no route to resolve.
472
519
  const histState = window.history.state;
473
520
  if (histState && histState[_ZQ_STATE_KEY] === 'substate') {
474
521
  const handled = this._fireSubstate(histState.key, histState.data, 'resolve');
475
522
  if (handled) return;
476
- // No listener handled it fall through to normal routing
523
+ // No listener handled it - fall through to normal routing
477
524
  }
478
525
 
479
526
  const fullPath = this.path;
@@ -510,7 +557,7 @@ class Router {
510
557
  const sameParams = _shallowEqual(params, from.params);
511
558
  const sameQuery = _shallowEqual(query, from.query);
512
559
  if (sameParams && sameQuery) {
513
- // Identical navigation nothing to do
560
+ // Identical navigation - nothing to do
514
561
  return;
515
562
  }
516
563
  }
@@ -598,6 +645,9 @@ class Router {
598
645
  }
599
646
  }
600
647
 
648
+ // Update z-active-route elements
649
+ this._updateActiveRoutes(path);
650
+
601
651
  // Run after guards
602
652
  for (const guard of this._guards.after) {
603
653
  await guard(to, from);
@@ -607,6 +657,32 @@ class Router {
607
657
  this._listeners.forEach(fn => fn(to, from));
608
658
  }
609
659
 
660
+ // --- Active route class management ----------------------------------------
661
+
662
+ /**
663
+ * Update all elements with z-active-route to toggle their active class
664
+ * based on the current path.
665
+ *
666
+ * Usage:
667
+ * <a z-link="/docs" z-active-route="/docs">Docs</a>
668
+ * <a z-link="/about" z-active-route="/about" z-active-class="selected">About</a>
669
+ * <a z-link="/" z-active-route="/" z-active-exact>Home</a>
670
+ */
671
+ _updateActiveRoutes(currentPath) {
672
+ if (typeof document === 'undefined') return;
673
+ const els = document.querySelectorAll('[z-active-route]');
674
+ for (let i = 0; i < els.length; i++) {
675
+ const el = els[i];
676
+ const route = el.getAttribute('z-active-route');
677
+ const cls = el.getAttribute('z-active-class') || 'active';
678
+ const exact = el.hasAttribute('z-active-exact');
679
+ const isActive = exact
680
+ ? currentPath === route
681
+ : (route === '/' ? currentPath === '/' : currentPath.startsWith(route));
682
+ el.classList.toggle(cls, isActive);
683
+ }
684
+ }
685
+
610
686
  // --- Destroy -------------------------------------------------------------
611
687
 
612
688
  destroy() {
@@ -615,6 +691,10 @@ class Router {
615
691
  window.removeEventListener(this._mode === 'hash' ? 'hashchange' : 'popstate', this._onNavEvent);
616
692
  this._onNavEvent = null;
617
693
  }
694
+ if (this._onPopState) {
695
+ window.removeEventListener('popstate', this._onPopState);
696
+ this._onPopState = null;
697
+ }
618
698
  if (this._onLinkClick) {
619
699
  document.removeEventListener('click', this._onLinkClick);
620
700
  this._onLinkClick = null;