@tanstack/form-core 1.3.4 → 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
  */