@tanstack/form-core 1.3.2 → 1.4.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/FieldApi.ts CHANGED
@@ -20,6 +20,7 @@ import type {
20
20
  FormValidateOrFn,
21
21
  } from './FormApi'
22
22
  import type {
23
+ ListenerCause,
23
24
  UpdateMetaOptions,
24
25
  ValidationCause,
25
26
  ValidationError,
@@ -349,7 +350,9 @@ export interface FieldListeners<
349
350
  TData extends DeepValue<TParentData, TName> = DeepValue<TParentData, TName>,
350
351
  > {
351
352
  onChange?: FieldListenerFn<TParentData, TName, TData>
353
+ onChangeDebounceMs?: number
352
354
  onBlur?: FieldListenerFn<TParentData, TName, TData>
355
+ onBlurDebounceMs?: number
353
356
  onMount?: FieldListenerFn<TParentData, TName, TData>
354
357
  onSubmit?: FieldListenerFn<TParentData, TName, TData>
355
358
  }
@@ -962,7 +965,10 @@ export class FieldApi<
962
965
  get state() {
963
966
  return this.store.state
964
967
  }
965
- timeoutIds: Record<ValidationCause, ReturnType<typeof setTimeout> | null>
968
+ timeoutIds: {
969
+ validations: Record<ValidationCause, ReturnType<typeof setTimeout> | null>
970
+ listeners: Record<ListenerCause, ReturnType<typeof setTimeout> | null>
971
+ }
966
972
 
967
973
  /**
968
974
  * Initializes a new `FieldApi` instance.
@@ -992,7 +998,10 @@ export class FieldApi<
992
998
  ) {
993
999
  this.form = opts.form as never
994
1000
  this.name = opts.name as never
995
- this.timeoutIds = {} as Record<ValidationCause, never>
1001
+ this.timeoutIds = {
1002
+ validations: {} as Record<ValidationCause, never>,
1003
+ listeners: {} as Record<ListenerCause, never>,
1004
+ }
996
1005
 
997
1006
  this.store = new Derived({
998
1007
  deps: [this.form.store],
@@ -1175,10 +1184,7 @@ export class FieldApi<
1175
1184
  setValue = (updater: Updater<TData>, options?: UpdateMetaOptions) => {
1176
1185
  this.form.setFieldValue(this.name, updater as never, options)
1177
1186
 
1178
- this.options.listeners?.onChange?.({
1179
- value: this.state.value,
1180
- fieldApi: this,
1181
- })
1187
+ this.triggerOnChangeListener()
1182
1188
 
1183
1189
  this.validate('change')
1184
1190
  }
@@ -1226,10 +1232,7 @@ export class FieldApi<
1226
1232
  ) => {
1227
1233
  this.form.pushFieldValue(this.name, value as any, opts)
1228
1234
 
1229
- this.options.listeners?.onChange?.({
1230
- value: this.state.value,
1231
- fieldApi: this,
1232
- })
1235
+ this.triggerOnChangeListener()
1233
1236
  }
1234
1237
 
1235
1238
  /**
@@ -1242,10 +1245,7 @@ export class FieldApi<
1242
1245
  ) => {
1243
1246
  this.form.insertFieldValue(this.name, index, value as any, opts)
1244
1247
 
1245
- this.options.listeners?.onChange?.({
1246
- value: this.state.value,
1247
- fieldApi: this,
1248
- })
1248
+ this.triggerOnChangeListener()
1249
1249
  }
1250
1250
 
1251
1251
  /**
@@ -1258,10 +1258,7 @@ export class FieldApi<
1258
1258
  ) => {
1259
1259
  this.form.replaceFieldValue(this.name, index, value as any, opts)
1260
1260
 
1261
- this.options.listeners?.onChange?.({
1262
- value: this.state.value,
1263
- fieldApi: this,
1264
- })
1261
+ this.triggerOnChangeListener()
1265
1262
  }
1266
1263
 
1267
1264
  /**
@@ -1270,10 +1267,7 @@ export class FieldApi<
1270
1267
  removeValue = (index: number, opts?: UpdateMetaOptions) => {
1271
1268
  this.form.removeFieldValue(this.name, index, opts)
1272
1269
 
1273
- this.options.listeners?.onChange?.({
1274
- value: this.state.value,
1275
- fieldApi: this,
1276
- })
1270
+ this.triggerOnChangeListener()
1277
1271
  }
1278
1272
 
1279
1273
  /**
@@ -1282,10 +1276,7 @@ export class FieldApi<
1282
1276
  swapValues = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) => {
1283
1277
  this.form.swapFieldValues(this.name, aIndex, bIndex, opts)
1284
1278
 
1285
- this.options.listeners?.onChange?.({
1286
- value: this.state.value,
1287
- fieldApi: this,
1288
- })
1279
+ this.triggerOnChangeListener()
1289
1280
  }
1290
1281
 
1291
1282
  /**
@@ -1294,10 +1285,7 @@ export class FieldApi<
1294
1285
  moveValue = (aIndex: number, bIndex: number, opts?: UpdateMetaOptions) => {
1295
1286
  this.form.moveFieldValues(this.name, aIndex, bIndex, opts)
1296
1287
 
1297
- this.options.listeners?.onChange?.({
1298
- value: this.state.value,
1299
- fieldApi: this,
1300
- })
1288
+ this.triggerOnChangeListener()
1301
1289
  }
1302
1290
 
1303
1291
  /**
@@ -1502,29 +1490,32 @@ export class FieldApi<
1502
1490
  let rawError!: ValidationError | undefined
1503
1491
  try {
1504
1492
  rawError = await new Promise((rawResolve, rawReject) => {
1505
- if (this.timeoutIds[validateObj.cause]) {
1506
- clearTimeout(this.timeoutIds[validateObj.cause]!)
1493
+ if (this.timeoutIds.validations[validateObj.cause]) {
1494
+ clearTimeout(this.timeoutIds.validations[validateObj.cause]!)
1507
1495
  }
1508
1496
 
1509
- this.timeoutIds[validateObj.cause] = setTimeout(async () => {
1510
- if (controller.signal.aborted) return rawResolve(undefined)
1511
- try {
1512
- rawResolve(
1513
- await this.runValidator({
1514
- validate: validateObj.validate,
1515
- value: {
1516
- value: field.store.state.value,
1517
- fieldApi: field,
1518
- signal: controller.signal,
1519
- validationSource: 'field',
1520
- },
1521
- type: 'validateAsync',
1522
- }),
1523
- )
1524
- } catch (e) {
1525
- rawReject(e)
1526
- }
1527
- }, validateObj.debounceMs)
1497
+ this.timeoutIds.validations[validateObj.cause] = setTimeout(
1498
+ async () => {
1499
+ if (controller.signal.aborted) return rawResolve(undefined)
1500
+ try {
1501
+ rawResolve(
1502
+ await this.runValidator({
1503
+ validate: validateObj.validate,
1504
+ value: {
1505
+ value: field.store.state.value,
1506
+ fieldApi: field,
1507
+ signal: controller.signal,
1508
+ validationSource: 'field',
1509
+ },
1510
+ type: 'validateAsync',
1511
+ }),
1512
+ )
1513
+ } catch (e) {
1514
+ rawReject(e)
1515
+ }
1516
+ },
1517
+ validateObj.debounceMs,
1518
+ )
1528
1519
  })
1529
1520
  } catch (e: unknown) {
1530
1521
  rawError = e as ValidationError
@@ -1633,10 +1624,7 @@ export class FieldApi<
1633
1624
  }
1634
1625
  this.validate('blur')
1635
1626
 
1636
- this.options.listeners?.onBlur?.({
1637
- value: this.state.value,
1638
- fieldApi: this,
1639
- })
1627
+ this.triggerOnBlurListener()
1640
1628
  }
1641
1629
 
1642
1630
  /**
@@ -1654,6 +1642,50 @@ export class FieldApi<
1654
1642
  }) as never,
1655
1643
  )
1656
1644
  }
1645
+
1646
+ private triggerOnBlurListener() {
1647
+ const debounceMs = this.options.listeners?.onBlurDebounceMs
1648
+
1649
+ if (debounceMs && debounceMs > 0) {
1650
+ if (this.timeoutIds.listeners.blur) {
1651
+ clearTimeout(this.timeoutIds.listeners.blur)
1652
+ }
1653
+
1654
+ this.timeoutIds.listeners.blur = setTimeout(() => {
1655
+ this.options.listeners?.onBlur?.({
1656
+ value: this.state.value,
1657
+ fieldApi: this,
1658
+ })
1659
+ }, debounceMs)
1660
+ } else {
1661
+ this.options.listeners?.onBlur?.({
1662
+ value: this.state.value,
1663
+ fieldApi: this,
1664
+ })
1665
+ }
1666
+ }
1667
+
1668
+ private triggerOnChangeListener() {
1669
+ const debounceMs = this.options.listeners?.onChangeDebounceMs
1670
+
1671
+ if (debounceMs && debounceMs > 0) {
1672
+ if (this.timeoutIds.listeners.change) {
1673
+ clearTimeout(this.timeoutIds.listeners.change)
1674
+ }
1675
+
1676
+ this.timeoutIds.listeners.change = setTimeout(() => {
1677
+ this.options.listeners?.onChange?.({
1678
+ value: this.state.value,
1679
+ fieldApi: this,
1680
+ })
1681
+ }, debounceMs)
1682
+ } else {
1683
+ this.options.listeners?.onChange?.({
1684
+ value: this.state.value,
1685
+ fieldApi: this,
1686
+ })
1687
+ }
1688
+ }
1657
1689
  }
1658
1690
 
1659
1691
  function normalizeError(rawError?: ValidationError) {
package/src/types.ts CHANGED
@@ -10,6 +10,11 @@ export type ValidationSource = 'form' | 'field'
10
10
  */
11
11
  export type ValidationCause = 'change' | 'blur' | 'submit' | 'mount' | 'server'
12
12
 
13
+ /**
14
+ * @private
15
+ */
16
+ export type ListenerCause = 'change' | 'blur' | 'submit' | 'mount'
17
+
13
18
  /**
14
19
  * @private
15
20
  */
package/src/util-types.ts CHANGED
@@ -21,35 +21,58 @@ export type Narrow<A> = Try<A, [], NarrowRaw<A>>
21
21
 
22
22
  type IsAny<T> = 0 extends 1 & T ? true : false
23
23
 
24
- export type ArrayAccessor<TPrefix extends string> = `${TPrefix}[${number}]`
24
+ export interface AnyDeepKeyAndValue {
25
+ key: string
26
+ value: any
27
+ }
28
+
29
+ export type ArrayAccessor<TParent extends AnyDeepKeyAndValue> =
30
+ `${TParent['key'] extends never ? '' : TParent['key']}[${number}]`
31
+
32
+ export interface ArrayDeepKeyAndValue<
33
+ in out TParent extends AnyDeepKeyAndValue,
34
+ in out T extends ReadonlyArray<any>,
35
+ > {
36
+ key: ArrayAccessor<TParent>
37
+ value: T[number] | Nullable<TParent['value']>
38
+ }
25
39
 
26
- export type DeepRecordArrayUnion<
40
+ export type DeepKeyAndValueArray<
41
+ TParent extends AnyDeepKeyAndValue,
27
42
  T extends ReadonlyArray<any>,
28
- TPrefix extends string,
29
43
  TAcc,
30
- > = DeepRecordUnion<
31
- T[number],
32
- ArrayAccessor<TPrefix>,
33
- TAcc | Record<ArrayAccessor<TPrefix>, T[number]>
44
+ > = DeepKeysAndValues<
45
+ NonNullable<T[number]>,
46
+ ArrayDeepKeyAndValue<TParent, T>,
47
+ TAcc | ArrayDeepKeyAndValue<TParent, T>
34
48
  >
35
49
 
36
50
  export type TupleAccessor<
37
- TPrefix extends string,
38
- TKey,
39
- > = `${TPrefix}[${TKey & string}]`
51
+ TParent extends AnyDeepKeyAndValue,
52
+ TKey extends string,
53
+ > = `${TParent['key'] extends never ? '' : TParent['key']}[${TKey}]`
54
+
55
+ export interface TupleDeepKeyAndValue<
56
+ in out TParent extends AnyDeepKeyAndValue,
57
+ in out T,
58
+ in out TKey extends AllTupleKeys<T>,
59
+ > {
60
+ key: TupleAccessor<TParent, TKey>
61
+ value: T[TKey] | Nullable<TParent['value']>
62
+ }
40
63
 
41
64
  export type AllTupleKeys<T> = T extends any ? keyof T & `${number}` : never
42
65
 
43
- export type DeepRecordTupleUnion<
66
+ export type DeepKeyAndValueTuple<
67
+ TParent extends AnyDeepKeyAndValue,
44
68
  T extends ReadonlyArray<any>,
45
- TPrefix extends string,
46
69
  TAcc,
47
70
  TAllKeys extends AllTupleKeys<T> = AllTupleKeys<T>,
48
71
  > = TAllKeys extends any
49
- ? DeepRecordUnion<
50
- T[TAllKeys],
51
- TupleAccessor<TPrefix, TAllKeys>,
52
- TAcc | Record<TupleAccessor<TPrefix, TAllKeys>, T[TAllKeys]>
72
+ ? DeepKeysAndValues<
73
+ NonNullable<T[TAllKeys]>,
74
+ TupleDeepKeyAndValue<TParent, T, TAllKeys>,
75
+ TAcc | TupleDeepKeyAndValue<TParent, T, TAllKeys>
53
76
  >
54
77
  : never
55
78
 
@@ -58,53 +81,85 @@ export type AllObjectKeys<T> = T extends any
58
81
  : never
59
82
 
60
83
  export type ObjectAccessor<
61
- TPrefix extends string,
84
+ TParent extends AnyDeepKeyAndValue,
62
85
  TKey extends string | number,
63
- > = TPrefix extends '' ? `${TKey}` : `${TPrefix}.${TKey}`
86
+ > = TParent['key'] extends never ? `${TKey}` : `${TParent['key']}.${TKey}`
87
+
88
+ export type Nullable<T> = T & (undefined | null)
89
+
90
+ export interface ObjectDeepKeyAndValue<
91
+ in out TParent extends AnyDeepKeyAndValue,
92
+ in out T,
93
+ in out TKey extends AllObjectKeys<T>,
94
+ > {
95
+ key: ObjectAccessor<TParent, TKey>
96
+ value: T[TKey] | Nullable<TParent['value']>
97
+ }
64
98
 
65
- export type DeepRecordObjectUnion<
99
+ export type DeepKeyAndValueObject<
100
+ TParent extends AnyDeepKeyAndValue,
66
101
  T,
67
- TPrefix extends string,
68
102
  TAcc,
69
103
  TAllKeys extends AllObjectKeys<T> = AllObjectKeys<T>,
70
104
  > = TAllKeys extends any
71
- ? DeepRecordUnion<
72
- T[TAllKeys],
73
- ObjectAccessor<TPrefix, TAllKeys>,
74
- TAcc | Record<ObjectAccessor<TPrefix, TAllKeys>, T[TAllKeys]>
105
+ ? DeepKeysAndValues<
106
+ NonNullable<T[TAllKeys]>,
107
+ ObjectDeepKeyAndValue<TParent, T, TAllKeys>,
108
+ TAcc | ObjectDeepKeyAndValue<TParent, T, TAllKeys>
75
109
  >
76
110
  : never
77
111
 
78
- export type DeepRecordUnion<T, TPrefix extends string = '', TAcc = never> =
112
+ export type UnknownAccessor<TParent extends AnyDeepKeyAndValue> =
113
+ TParent['key'] extends never ? string : `${TParent['key']}.${string}`
114
+
115
+ export interface UnknownDeepKeyAndValue<TParent extends AnyDeepKeyAndValue> {
116
+ key: UnknownAccessor<TParent>
117
+ value: unknown
118
+ }
119
+
120
+ export type DeepKeyAndValueUnknown<TParent extends AnyDeepKeyAndValue> =
121
+ UnknownDeepKeyAndValue<TParent>
122
+
123
+ export type DeepKeysAndValues<
124
+ T,
125
+ TParent extends AnyDeepKeyAndValue = never,
126
+ TAcc = never,
127
+ > =
79
128
  IsAny<T> extends true
80
129
  ? T
81
130
  : T extends string | number | boolean | bigint | Date
82
131
  ? TAcc
83
132
  : T extends ReadonlyArray<any>
84
133
  ? number extends T['length']
85
- ? DeepRecordArrayUnion<T, TPrefix, TAcc>
86
- : DeepRecordTupleUnion<T, TPrefix, TAcc>
87
- : T extends object
88
- ? DeepRecordObjectUnion<T, TPrefix, TAcc>
89
- : TAcc
134
+ ? DeepKeyAndValueArray<TParent, T, TAcc>
135
+ : DeepKeyAndValueTuple<TParent, T, TAcc>
136
+ : keyof T extends never
137
+ ? TAcc | DeepKeyAndValueUnknown<TParent>
138
+ : T extends object
139
+ ? DeepKeyAndValueObject<TParent, T, TAcc>
140
+ : TAcc
90
141
 
91
142
  export type DeepRecord<T> = {
92
- [TRecord in DeepRecordUnion<T> as keyof TRecord]: TRecord[keyof TRecord]
143
+ [TRecord in DeepKeysAndValues<T> extends AnyDeepKeyAndValue
144
+ ? DeepKeysAndValues<T>
145
+ : never as TRecord['key']]: TRecord['value']
93
146
  }
94
147
 
95
- type UnionKeys<T> = T extends any ? keyof T : never
96
-
97
148
  /**
98
149
  * The keys of an object or array, deeply nested.
99
150
  */
100
151
  export type DeepKeys<T> = unknown extends T
101
152
  ? string
102
- : UnionKeys<DeepRecordUnion<T>> & string
153
+ : DeepKeysAndValues<T> extends AnyDeepKeyAndValue
154
+ ? DeepKeysAndValues<T>['key']
155
+ : never
103
156
 
104
157
  /**
105
158
  * Infer the type of a deeply nested property within an object or an array.
106
159
  */
107
160
  export type DeepValue<TValue, TAccessor> =
108
161
  DeepRecord<TValue> extends infer TDeepRecord
109
- ? TDeepRecord[TAccessor & keyof TDeepRecord]
162
+ ? TAccessor extends keyof TDeepRecord
163
+ ? TDeepRecord[TAccessor]
164
+ : never
110
165
  : never