@tanstack/history 1.85.3 → 1.90.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/src/index.ts CHANGED
@@ -5,21 +5,36 @@
5
5
  export interface NavigateOptions {
6
6
  ignoreBlocker?: boolean
7
7
  }
8
+
9
+ type SubscriberHistoryAction =
10
+ | {
11
+ type: HistoryAction | 'ROLLBACK'
12
+ }
13
+ | {
14
+ type: 'GO'
15
+ index: number
16
+ }
17
+
18
+ type SubscriberArgs = {
19
+ location: HistoryLocation
20
+ action: SubscriberHistoryAction
21
+ }
22
+
8
23
  export interface RouterHistory {
9
24
  location: HistoryLocation
10
25
  length: number
11
- subscribers: Set<(opts: { location: HistoryLocation }) => void>
12
- subscribe: (cb: (opts: { location: HistoryLocation }) => void) => () => void
26
+ subscribers: Set<(opts: SubscriberArgs) => void>
27
+ subscribe: (cb: (opts: SubscriberArgs) => void) => () => void
13
28
  push: (path: string, state?: any, navigateOpts?: NavigateOptions) => void
14
29
  replace: (path: string, state?: any, navigateOpts?: NavigateOptions) => void
15
30
  go: (index: number, navigateOpts?: NavigateOptions) => void
16
31
  back: (navigateOpts?: NavigateOptions) => void
17
32
  forward: (navigateOpts?: NavigateOptions) => void
18
33
  createHref: (href: string) => string
19
- block: (blocker: BlockerFn) => () => void
34
+ block: (blocker: NavigationBlocker) => () => void
20
35
  flush: () => void
21
36
  destroy: () => void
22
- notify: () => void
37
+ notify: (action: SubscriberHistoryAction) => void
23
38
  _ignoreSubscribers?: boolean
24
39
  }
25
40
 
@@ -40,25 +55,46 @@ export interface HistoryState {
40
55
 
41
56
  type ShouldAllowNavigation = any
42
57
 
43
- export type BlockerFn = () =>
44
- | Promise<ShouldAllowNavigation>
45
- | ShouldAllowNavigation
58
+ export type HistoryAction =
59
+ | 'PUSH'
60
+ | 'POP'
61
+ | 'REPLACE'
62
+ | 'FORWARD'
63
+ | 'BACK'
64
+ | 'GO'
65
+
66
+ export type BlockerFnArgs = {
67
+ currentLocation: HistoryLocation
68
+ nextLocation: HistoryLocation
69
+ action: HistoryAction
70
+ }
46
71
 
47
- const pushStateEvent = 'pushstate'
48
- const popStateEvent = 'popstate'
49
- const beforeUnloadEvent = 'beforeunload'
72
+ export type BlockerFn = (
73
+ args: BlockerFnArgs,
74
+ ) => Promise<ShouldAllowNavigation> | ShouldAllowNavigation
50
75
 
51
- const beforeUnloadListener = (event: Event) => {
52
- event.preventDefault()
53
- // @ts-expect-error
54
- return (event.returnValue = '')
76
+ export type NavigationBlocker = {
77
+ blockerFn: BlockerFn
78
+ enableBeforeUnload?: (() => boolean) | boolean
55
79
  }
56
80
 
57
- const stopBlocking = () => {
58
- removeEventListener(beforeUnloadEvent, beforeUnloadListener, {
59
- capture: true,
60
- })
61
- }
81
+ type TryNavigateArgs = {
82
+ task: () => void
83
+ type: 'PUSH' | 'REPLACE' | 'BACK' | 'FORWARD' | 'GO'
84
+ navigateOpts?: NavigateOptions
85
+ } & (
86
+ | {
87
+ type: 'PUSH' | 'REPLACE'
88
+ path: string
89
+ state: any
90
+ }
91
+ | {
92
+ type: 'BACK' | 'FORWARD' | 'GO'
93
+ }
94
+ )
95
+
96
+ const popStateEvent = 'popstate'
97
+ const beforeUnloadEvent = 'beforeunload'
62
98
 
63
99
  export function createHistory(opts: {
64
100
  getLocation: () => HistoryLocation
@@ -66,32 +102,54 @@ export function createHistory(opts: {
66
102
  pushState: (path: string, state: any) => void
67
103
  replaceState: (path: string, state: any) => void
68
104
  go: (n: number) => void
69
- back: () => void
70
- forward: () => void
105
+ back: (ignoreBlocker: boolean) => void
106
+ forward: (ignoreBlocker: boolean) => void
71
107
  createHref: (path: string) => string
72
108
  flush?: () => void
73
109
  destroy?: () => void
74
110
  onBlocked?: (onUpdate: () => void) => void
111
+ getBlockers?: () => Array<NavigationBlocker>
112
+ setBlockers?: (blockers: Array<NavigationBlocker>) => void
75
113
  }): RouterHistory {
76
114
  let location = opts.getLocation()
77
- const subscribers = new Set<(opts: { location: HistoryLocation }) => void>()
78
- let blockers: Array<BlockerFn> = []
115
+ const subscribers = new Set<(opts: SubscriberArgs) => void>()
79
116
 
80
- const notify = () => {
117
+ const notify = (action: SubscriberHistoryAction) => {
81
118
  location = opts.getLocation()
82
- subscribers.forEach((subscriber) => subscriber({ location }))
119
+ subscribers.forEach((subscriber) => subscriber({ location, action }))
83
120
  }
84
121
 
85
- const tryNavigation = async (
86
- task: () => void,
87
- navigateOpts?: NavigateOptions,
88
- ) => {
122
+ const _notifyRollback = () => {
123
+ location = opts.getLocation()
124
+ subscribers.forEach((subscriber) =>
125
+ subscriber({ location, action: { type: 'ROLLBACK' } }),
126
+ )
127
+ }
128
+
129
+ const tryNavigation = async ({
130
+ task,
131
+ navigateOpts,
132
+ ...actionInfo
133
+ }: TryNavigateArgs) => {
89
134
  const ignoreBlocker = navigateOpts?.ignoreBlocker ?? false
90
- if (!ignoreBlocker && typeof document !== 'undefined' && blockers.length) {
135
+ if (ignoreBlocker) {
136
+ task()
137
+ return
138
+ }
139
+
140
+ const blockers = opts.getBlockers?.() ?? []
141
+ const isPushOrReplace =
142
+ actionInfo.type === 'PUSH' || actionInfo.type === 'REPLACE'
143
+ if (typeof document !== 'undefined' && blockers.length && isPushOrReplace) {
91
144
  for (const blocker of blockers) {
92
- const allowed = await blocker()
93
- if (!allowed) {
94
- opts.onBlocked?.(notify)
145
+ const nextLocation = parseHref(actionInfo.path, actionInfo.state)
146
+ const isBlocked = await blocker.blockerFn({
147
+ currentLocation: location,
148
+ nextLocation,
149
+ action: actionInfo.type,
150
+ })
151
+ if (isBlocked) {
152
+ opts.onBlocked?.(_notifyRollback)
95
153
  return
96
154
  }
97
155
  }
@@ -108,7 +166,7 @@ export function createHistory(opts: {
108
166
  return opts.getLength()
109
167
  },
110
168
  subscribers,
111
- subscribe: (cb: (opts: { location: HistoryLocation }) => void) => {
169
+ subscribe: (cb: (opts: SubscriberArgs) => void) => {
112
170
  subscribers.add(cb)
113
171
 
114
172
  return () => {
@@ -117,52 +175,69 @@ export function createHistory(opts: {
117
175
  },
118
176
  push: (path, state, navigateOpts) => {
119
177
  state = assignKey(state)
120
- tryNavigation(() => {
121
- opts.pushState(path, state)
122
- notify()
123
- }, navigateOpts)
178
+ tryNavigation({
179
+ task: () => {
180
+ opts.pushState(path, state)
181
+ notify({ type: 'PUSH' })
182
+ },
183
+ navigateOpts,
184
+ type: 'PUSH',
185
+ path,
186
+ state,
187
+ })
124
188
  },
125
189
  replace: (path, state, navigateOpts) => {
126
190
  state = assignKey(state)
127
- tryNavigation(() => {
128
- opts.replaceState(path, state)
129
- notify()
130
- }, navigateOpts)
191
+ tryNavigation({
192
+ task: () => {
193
+ opts.replaceState(path, state)
194
+ notify({ type: 'REPLACE' })
195
+ },
196
+ navigateOpts,
197
+ type: 'REPLACE',
198
+ path,
199
+ state,
200
+ })
131
201
  },
132
202
  go: (index, navigateOpts) => {
133
- tryNavigation(() => {
134
- opts.go(index)
135
- notify()
136
- }, navigateOpts)
203
+ tryNavigation({
204
+ task: () => {
205
+ opts.go(index)
206
+ notify({ type: 'GO', index })
207
+ },
208
+ navigateOpts,
209
+ type: 'GO',
210
+ })
137
211
  },
138
212
  back: (navigateOpts) => {
139
- tryNavigation(() => {
140
- opts.back()
141
- notify()
142
- }, navigateOpts)
213
+ tryNavigation({
214
+ task: () => {
215
+ opts.back(navigateOpts?.ignoreBlocker ?? false)
216
+ notify({ type: 'BACK' })
217
+ },
218
+ navigateOpts,
219
+ type: 'BACK',
220
+ })
143
221
  },
144
222
  forward: (navigateOpts) => {
145
- tryNavigation(() => {
146
- opts.forward()
147
- notify()
148
- }, navigateOpts)
223
+ tryNavigation({
224
+ task: () => {
225
+ opts.forward(navigateOpts?.ignoreBlocker ?? false)
226
+ notify({ type: 'FORWARD' })
227
+ },
228
+ navigateOpts,
229
+ type: 'FORWARD',
230
+ })
149
231
  },
150
232
  createHref: (str) => opts.createHref(str),
151
233
  block: (blocker) => {
152
- blockers.push(blocker)
153
-
154
- if (blockers.length === 1) {
155
- addEventListener(beforeUnloadEvent, beforeUnloadListener, {
156
- capture: true,
157
- })
158
- }
234
+ if (!opts.setBlockers) return () => {}
235
+ const blockers = opts.getBlockers?.() ?? []
236
+ opts.setBlockers([...blockers, blocker])
159
237
 
160
238
  return () => {
161
- blockers = blockers.filter((b) => b !== blocker)
162
-
163
- if (!blockers.length) {
164
- stopBlocking()
165
- }
239
+ const blockers = opts.getBlockers?.() ?? []
240
+ opts.setBlockers?.(blockers.filter((b) => b !== blocker))
166
241
  }
167
242
  },
168
243
  flush: () => opts.flush?.(),
@@ -209,6 +284,11 @@ export function createBrowserHistory(opts?: {
209
284
  const originalPushState = win.history.pushState
210
285
  const originalReplaceState = win.history.replaceState
211
286
 
287
+ let blockers: Array<NavigationBlocker> = []
288
+ const _getBlockers = () => blockers
289
+ const _setBlockers = (newBlockers: Array<NavigationBlocker>) =>
290
+ (blockers = newBlockers)
291
+
212
292
  const createHref = opts?.createHref ?? ((path) => path)
213
293
  const parseLocation =
214
294
  opts?.parseLocation ??
@@ -221,6 +301,10 @@ export function createBrowserHistory(opts?: {
221
301
  let currentLocation = parseLocation()
222
302
  let rollbackLocation: HistoryLocation | undefined
223
303
 
304
+ let ignoreNextPop = false
305
+ let skipBlockerNextPop = false
306
+ let ignoreNextBeforeUnload = false
307
+
224
308
  const getLocation = () => currentLocation
225
309
 
226
310
  let next:
@@ -293,7 +377,74 @@ export function createBrowserHistory(opts?: {
293
377
 
294
378
  const onPushPop = () => {
295
379
  currentLocation = parseLocation()
296
- history.notify()
380
+ history.notify({ type: 'POP' })
381
+ }
382
+
383
+ const onPushPopEvent = async () => {
384
+ if (ignoreNextPop) {
385
+ ignoreNextPop = false
386
+ return
387
+ }
388
+
389
+ if (skipBlockerNextPop) {
390
+ skipBlockerNextPop = false
391
+ } else {
392
+ const blockers = _getBlockers()
393
+ if (typeof document !== 'undefined' && blockers.length) {
394
+ for (const blocker of blockers) {
395
+ const nextLocation = parseLocation()
396
+ const isBlocked = await blocker.blockerFn({
397
+ currentLocation,
398
+ nextLocation,
399
+ action: 'POP',
400
+ })
401
+ if (isBlocked) {
402
+ ignoreNextPop = true
403
+ win.history.go(1)
404
+ history.notify({ type: 'POP' })
405
+ return
406
+ }
407
+ }
408
+ }
409
+ }
410
+
411
+ currentLocation = parseLocation()
412
+ history.notify({ type: 'POP' })
413
+ }
414
+
415
+ const onBeforeUnload = (e: BeforeUnloadEvent) => {
416
+ if (ignoreNextBeforeUnload) {
417
+ ignoreNextBeforeUnload = false
418
+ return
419
+ }
420
+
421
+ let shouldBlock = false
422
+
423
+ // If one blocker has a non-disabled beforeUnload, we should block
424
+ const blockers = _getBlockers()
425
+ if (typeof document !== 'undefined' && blockers.length) {
426
+ for (const blocker of blockers) {
427
+ const shouldHaveBeforeUnload = blocker.enableBeforeUnload ?? true
428
+ if (shouldHaveBeforeUnload === true) {
429
+ shouldBlock = true
430
+ break
431
+ }
432
+
433
+ if (
434
+ typeof shouldHaveBeforeUnload === 'function' &&
435
+ shouldHaveBeforeUnload() === true
436
+ ) {
437
+ shouldBlock = true
438
+ break
439
+ }
440
+ }
441
+ }
442
+
443
+ if (shouldBlock) {
444
+ e.preventDefault()
445
+ return (e.returnValue = '')
446
+ }
447
+ return
297
448
  }
298
449
 
299
450
  const history = createHistory({
@@ -301,16 +452,26 @@ export function createBrowserHistory(opts?: {
301
452
  getLength: () => win.history.length,
302
453
  pushState: (href, state) => queueHistoryAction('push', href, state),
303
454
  replaceState: (href, state) => queueHistoryAction('replace', href, state),
304
- back: () => win.history.back(),
305
- forward: () => win.history.forward(),
455
+ back: (ignoreBlocker) => {
456
+ if (ignoreBlocker) skipBlockerNextPop = true
457
+ ignoreNextBeforeUnload = true
458
+ return win.history.back()
459
+ },
460
+ forward: (ignoreBlocker) => {
461
+ if (ignoreBlocker) skipBlockerNextPop = true
462
+ ignoreNextBeforeUnload = true
463
+ win.history.forward()
464
+ },
306
465
  go: (n) => win.history.go(n),
307
466
  createHref: (href) => createHref(href),
308
467
  flush,
309
468
  destroy: () => {
310
469
  win.history.pushState = originalPushState
311
470
  win.history.replaceState = originalReplaceState
312
- win.removeEventListener(pushStateEvent, onPushPop)
313
- win.removeEventListener(popStateEvent, onPushPop)
471
+ win.removeEventListener(beforeUnloadEvent, onBeforeUnload, {
472
+ capture: true,
473
+ })
474
+ win.removeEventListener(popStateEvent, onPushPopEvent)
314
475
  },
315
476
  onBlocked: (onUpdate) => {
316
477
  // If a navigation is blocked, we need to rollback the location
@@ -321,19 +482,21 @@ export function createBrowserHistory(opts?: {
321
482
  onUpdate()
322
483
  }
323
484
  },
485
+ getBlockers: _getBlockers,
486
+ setBlockers: _setBlockers,
324
487
  })
325
488
 
326
- win.addEventListener(pushStateEvent, onPushPop)
327
- win.addEventListener(popStateEvent, onPushPop)
489
+ win.addEventListener(beforeUnloadEvent, onBeforeUnload, { capture: true })
490
+ win.addEventListener(popStateEvent, onPushPopEvent)
328
491
 
329
492
  win.history.pushState = function (...args: Array<any>) {
330
- const res = originalPushState.apply(win.history, args)
493
+ const res = originalPushState.apply(win.history, args as any)
331
494
  if (!history._ignoreSubscribers) onPushPop()
332
495
  return res
333
496
  }
334
497
 
335
498
  win.history.replaceState = function (...args: Array<any>) {
336
- const res = originalReplaceState.apply(win.history, args)
499
+ const res = originalReplaceState.apply(win.history, args as any)
337
500
  if (!history._ignoreSubscribers) onPushPop()
338
501
  return res
339
502
  }