@tanstack/react-router 1.89.2 → 1.91.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.
@@ -1,92 +1,296 @@
1
1
  import * as React from 'react'
2
2
  import { useRouter } from './useRouter'
3
- import type { BlockerFn } from '@tanstack/history'
4
- import type { ReactNode } from './route'
3
+ import type {
4
+ BlockerFnArgs,
5
+ HistoryAction,
6
+ HistoryLocation,
7
+ } from '@tanstack/history'
8
+ import type { AnyRoute } from './route'
9
+ import type { ParseRoute } from './routeInfo'
10
+ import type { AnyRouter, RegisteredRouter } from './router'
5
11
 
6
- type BlockerResolver = {
7
- status: 'idle' | 'blocked'
8
- proceed: () => void
9
- reset: () => void
12
+ interface ShouldBlockFnLocation<
13
+ out TRouteId,
14
+ out TFullPath,
15
+ out TAllParams,
16
+ out TFullSearchSchema,
17
+ > {
18
+ routeId: TRouteId
19
+ fullPath: TFullPath
20
+ pathname: string
21
+ params: TAllParams
22
+ search: TFullSearchSchema
10
23
  }
11
24
 
12
- type BlockerOpts = {
13
- blockerFn?: BlockerFn
25
+ type AnyShouldBlockFnLocation = ShouldBlockFnLocation<any, any, any, any>
26
+ type MakeShouldBlockFnLocationUnion<
27
+ TRouter extends AnyRouter = RegisteredRouter,
28
+ TRoute extends AnyRoute = ParseRoute<TRouter['routeTree']>,
29
+ > = TRoute extends any
30
+ ? ShouldBlockFnLocation<
31
+ TRoute['id'],
32
+ TRoute['fullPath'],
33
+ TRoute['types']['allParams'],
34
+ TRoute['types']['fullSearchSchema']
35
+ >
36
+ : never
37
+
38
+ type BlockerResolver<TRouter extends AnyRouter = RegisteredRouter> =
39
+ | {
40
+ status: 'blocked'
41
+ current: MakeShouldBlockFnLocationUnion<TRouter>
42
+ next: MakeShouldBlockFnLocationUnion<TRouter>
43
+ action: HistoryAction
44
+ proceed: () => void
45
+ reset: () => void
46
+ }
47
+ | {
48
+ status: 'idle'
49
+ current: undefined
50
+ next: undefined
51
+ action: undefined
52
+ proceed: undefined
53
+ reset: undefined
54
+ }
55
+
56
+ type ShouldBlockFnArgs<TRouter extends AnyRouter = RegisteredRouter> = {
57
+ current: MakeShouldBlockFnLocationUnion<TRouter>
58
+ next: MakeShouldBlockFnLocationUnion<TRouter>
59
+ action: HistoryAction
60
+ }
61
+
62
+ export type ShouldBlockFn<TRouter extends AnyRouter = RegisteredRouter> = (
63
+ args: ShouldBlockFnArgs<TRouter>,
64
+ ) => boolean | Promise<boolean>
65
+ export type UseBlockerOpts<
66
+ TRouter extends AnyRouter = RegisteredRouter,
67
+ TWithResolver extends boolean = boolean,
68
+ > = {
69
+ shouldBlockFn: ShouldBlockFn<TRouter>
70
+ enableBeforeUnload?: boolean | (() => boolean)
71
+ disabled?: boolean
72
+ withResolver?: TWithResolver
73
+ }
74
+
75
+ type LegacyBlockerFn = () => Promise<any> | any
76
+ type LegacyBlockerOpts = {
77
+ blockerFn?: LegacyBlockerFn
14
78
  condition?: boolean | any
15
79
  }
16
80
 
17
- export function useBlocker(blockerFnOrOpts?: BlockerOpts): BlockerResolver
81
+ function _resolveBlockerOpts(
82
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
83
+ condition?: boolean | any,
84
+ ): UseBlockerOpts {
85
+ if (opts === undefined) {
86
+ return {
87
+ shouldBlockFn: () => true,
88
+ withResolver: false,
89
+ }
90
+ }
91
+
92
+ if ('shouldBlockFn' in opts) {
93
+ return opts
94
+ }
95
+
96
+ if (typeof opts === 'function') {
97
+ const shouldBlock = Boolean(condition ?? true)
98
+
99
+ const _customBlockerFn = async () => {
100
+ if (shouldBlock) return await opts()
101
+ return false
102
+ }
103
+
104
+ return {
105
+ shouldBlockFn: _customBlockerFn,
106
+ enableBeforeUnload: shouldBlock,
107
+ withResolver: false,
108
+ }
109
+ }
110
+
111
+ const shouldBlock = Boolean(opts.condition ?? true)
112
+ const fn = opts.blockerFn
113
+
114
+ const _customBlockerFn = async () => {
115
+ if (shouldBlock && fn !== undefined) {
116
+ return await fn()
117
+ }
118
+ return shouldBlock
119
+ }
120
+
121
+ return {
122
+ shouldBlockFn: _customBlockerFn,
123
+ enableBeforeUnload: shouldBlock,
124
+ withResolver: fn === undefined,
125
+ }
126
+ }
127
+
128
+ export function useBlocker<
129
+ TRouter extends AnyRouter = RegisteredRouter,
130
+ TWithResolver extends boolean = false,
131
+ >(
132
+ opts: UseBlockerOpts<TRouter, TWithResolver>,
133
+ ): TWithResolver extends true ? BlockerResolver<TRouter> : void
134
+
135
+ /**
136
+ * @deprecated Use the shouldBlockFn property instead
137
+ */
138
+ export function useBlocker(blockerFnOrOpts?: LegacyBlockerOpts): BlockerResolver
18
139
 
19
140
  /**
20
- * @deprecated Use the BlockerOpts object syntax instead
141
+ * @deprecated Use the UseBlockerOpts object syntax instead
21
142
  */
22
143
  export function useBlocker(
23
- blockerFn?: BlockerFn,
144
+ blockerFn?: LegacyBlockerFn,
24
145
  condition?: boolean | any,
25
146
  ): BlockerResolver
26
147
 
27
148
  export function useBlocker(
28
- blockerFnOrOpts?: BlockerFn | BlockerOpts,
149
+ opts?: UseBlockerOpts | LegacyBlockerOpts | LegacyBlockerFn,
29
150
  condition?: boolean | any,
30
- ): BlockerResolver {
31
- const { blockerFn, blockerCondition } = blockerFnOrOpts
32
- ? typeof blockerFnOrOpts === 'function'
33
- ? { blockerFn: blockerFnOrOpts, blockerCondition: condition ?? true }
34
- : {
35
- blockerFn: blockerFnOrOpts.blockerFn,
36
- blockerCondition: blockerFnOrOpts.condition ?? true,
37
- }
38
- : { blockerFn: undefined, blockerCondition: condition ?? true }
39
- const { history } = useRouter()
151
+ ): BlockerResolver | void {
152
+ const {
153
+ shouldBlockFn,
154
+ enableBeforeUnload = true,
155
+ disabled = false,
156
+ withResolver = false,
157
+ } = _resolveBlockerOpts(opts, condition)
158
+
159
+ const router = useRouter()
160
+ const { history } = router
40
161
 
41
162
  const [resolver, setResolver] = React.useState<BlockerResolver>({
42
163
  status: 'idle',
43
- proceed: () => {},
44
- reset: () => {},
164
+ current: undefined,
165
+ next: undefined,
166
+ action: undefined,
167
+ proceed: undefined,
168
+ reset: undefined,
45
169
  })
46
170
 
47
171
  React.useEffect(() => {
48
- const blockerFnComposed = async () => {
49
- // If a function is provided, it takes precedence over the promise blocker
50
- if (blockerFn) {
51
- return await blockerFn()
172
+ const blockerFnComposed = async (blockerFnArgs: BlockerFnArgs) => {
173
+ function getLocation(
174
+ location: HistoryLocation,
175
+ ): AnyShouldBlockFnLocation {
176
+ const parsedLocation = router.parseLocation(undefined, location)
177
+ const matchedRoutes = router.getMatchedRoutes(parsedLocation)
178
+ if (matchedRoutes.foundRoute === undefined) {
179
+ throw new Error(`No route found for location ${location.href}`)
180
+ }
181
+ return {
182
+ routeId: matchedRoutes.foundRoute.id,
183
+ fullPath: matchedRoutes.foundRoute.fullPath,
184
+ pathname: parsedLocation.pathname,
185
+ params: matchedRoutes.routeParams,
186
+ search: parsedLocation.search,
187
+ }
188
+ }
189
+
190
+ const current = getLocation(blockerFnArgs.currentLocation)
191
+ const next = getLocation(blockerFnArgs.nextLocation)
192
+
193
+ const shouldBlock = await shouldBlockFn({
194
+ action: blockerFnArgs.action,
195
+ current,
196
+ next,
197
+ })
198
+ if (!withResolver) {
199
+ return shouldBlock
200
+ }
201
+
202
+ if (!shouldBlock) {
203
+ return false
52
204
  }
53
205
 
54
206
  const promise = new Promise<boolean>((resolve) => {
55
207
  setResolver({
56
208
  status: 'blocked',
57
- proceed: () => resolve(true),
58
- reset: () => resolve(false),
209
+ current,
210
+ next,
211
+ action: blockerFnArgs.action,
212
+ proceed: () => resolve(false),
213
+ reset: () => resolve(true),
59
214
  })
60
215
  })
61
216
 
62
217
  const canNavigateAsync = await promise
63
-
64
218
  setResolver({
65
219
  status: 'idle',
66
- proceed: () => {},
67
- reset: () => {},
220
+ current: undefined,
221
+ next: undefined,
222
+ action: undefined,
223
+ proceed: undefined,
224
+ reset: undefined,
68
225
  })
69
226
 
70
227
  return canNavigateAsync
71
228
  }
72
229
 
73
- return !blockerCondition ? undefined : history.block(blockerFnComposed)
74
- }, [blockerFn, blockerCondition, history])
230
+ return disabled
231
+ ? undefined
232
+ : history.block({ blockerFn: blockerFnComposed, enableBeforeUnload })
233
+ }, [shouldBlockFn, enableBeforeUnload, disabled, withResolver, history])
75
234
 
76
235
  return resolver
77
236
  }
78
237
 
79
- export function Block({ blockerFn, condition, children }: PromptProps) {
80
- const resolver = useBlocker({ blockerFn, condition })
238
+ const _resolvePromptBlockerArgs = (
239
+ props: PromptProps | LegacyPromptProps,
240
+ ): UseBlockerOpts => {
241
+ if ('shouldBlockFn' in props) {
242
+ return { ...props }
243
+ }
244
+
245
+ const shouldBlock = Boolean(props.condition ?? true)
246
+ const fn = props.blockerFn
247
+
248
+ const _customBlockerFn = async () => {
249
+ if (shouldBlock && fn !== undefined) {
250
+ return await fn()
251
+ }
252
+ return shouldBlock
253
+ }
254
+
255
+ return {
256
+ shouldBlockFn: _customBlockerFn,
257
+ enableBeforeUnload: shouldBlock,
258
+ withResolver: fn === undefined,
259
+ }
260
+ }
261
+
262
+ export function Block<
263
+ TRouter extends AnyRouter = RegisteredRouter,
264
+ TWithResolver extends boolean = boolean,
265
+ >(opts: PromptProps<TRouter, TWithResolver>): React.ReactNode
266
+
267
+ /**
268
+ * @deprecated Use the UseBlockerOpts property instead
269
+ */
270
+ export function Block(opts: LegacyPromptProps): React.ReactNode
271
+
272
+ export function Block(opts: PromptProps | LegacyPromptProps): React.ReactNode {
273
+ const { children, ...rest } = opts
274
+ const args = _resolvePromptBlockerArgs(rest)
275
+
276
+ const resolver = useBlocker(args)
81
277
  return children
82
278
  ? typeof children === 'function'
83
- ? children(resolver)
279
+ ? children(resolver as any)
84
280
  : children
85
281
  : null
86
282
  }
87
283
 
88
- export type PromptProps = {
89
- blockerFn?: BlockerFn
284
+ type LegacyPromptProps = {
285
+ blockerFn?: LegacyBlockerFn
90
286
  condition?: boolean | any
91
- children?: ReactNode | (({ proceed, reset }: BlockerResolver) => ReactNode)
287
+ children?: React.ReactNode | ((params: BlockerResolver) => React.ReactNode)
288
+ }
289
+
290
+ type PromptProps<
291
+ TRouter extends AnyRouter = RegisteredRouter,
292
+ TWithResolver extends boolean = boolean,
293
+ TParams = TWithResolver extends true ? BlockerResolver<TRouter> : void,
294
+ > = UseBlockerOpts<TRouter, TWithResolver> & {
295
+ children?: React.ReactNode | ((params: TParams) => React.ReactNode)
92
296
  }
package/src/utils.ts CHANGED
@@ -111,6 +111,14 @@ export type PartialMergeAll<TUnion> =
111
111
  | ExtractPrimitives<TUnion>
112
112
  | PartialMergeAllObject<TUnion>
113
113
 
114
+ export type Constrain<T, TConstraint, TDefault = TConstraint> =
115
+ | (T extends TConstraint ? T : never)
116
+ | TDefault
117
+
118
+ export type ConstrainLiteral<T, TConstraint, TDefault = TConstraint> =
119
+ | (T & TConstraint)
120
+ | TDefault
121
+
114
122
  /**
115
123
  * To be added to router types
116
124
  */
@@ -139,10 +147,6 @@ export type MergeAll<TUnion> =
139
147
  | MergeAllObjects<TUnion>
140
148
  | ExtractPrimitives<TUnion>
141
149
 
142
- export type Constrain<T, TConstraint, TDefault = TConstraint> =
143
- | (T extends TConstraint ? T : never)
144
- | TDefault
145
-
146
150
  export type ValidateJSON<T> = ((...args: Array<any>) => any) extends T
147
151
  ? unknown extends T
148
152
  ? never
@@ -359,7 +363,7 @@ export type StrictOrFrom<
359
363
  strict: TStrict
360
364
  }
361
365
  : {
362
- from: StringLiteral<Constrain<TFrom, RouteIds<TRouter['routeTree']>>>
366
+ from: ConstrainLiteral<TFrom, RouteIds<TRouter['routeTree']>>
363
367
  strict?: TStrict
364
368
  }
365
369