@tanstack/form-core 0.20.1 → 0.20.3

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/FormApi.ts CHANGED
@@ -370,6 +370,59 @@ export class FormApi<
370
370
  return fieldErrorMapMap.flat()
371
371
  }
372
372
 
373
+ validateArrayFieldsStartingFrom = async <TField extends DeepKeys<TFormData>>(
374
+ field: TField,
375
+ index: number,
376
+ cause: ValidationCause,
377
+ ) => {
378
+ const currentValue = this.getFieldValue(field)
379
+
380
+ const lastIndex = Array.isArray(currentValue)
381
+ ? Math.max(currentValue.length - 1, 0)
382
+ : null
383
+
384
+ // We have to validate all fields that have shifted (at least the current field)
385
+ const fieldKeysToValidate = [`${field}[${index}]`]
386
+ for (let i = index + 1; i <= (lastIndex ?? 0); i++) {
387
+ fieldKeysToValidate.push(`${field}[${i}]`)
388
+ }
389
+
390
+ // We also have to include all fields that are nested in the shifted fields
391
+ const fieldsToValidate = Object.keys(this.fieldInfo).filter((fieldKey) =>
392
+ fieldKeysToValidate.some((key) => fieldKey.startsWith(key)),
393
+ ) as DeepKeys<TFormData>[]
394
+
395
+ // Validate the fields
396
+ const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
397
+ this.store.batch(() => {
398
+ fieldsToValidate.forEach((nestedField) => {
399
+ fieldValidationPromises.push(
400
+ Promise.resolve().then(() => this.validateField(nestedField, cause)),
401
+ )
402
+ })
403
+ })
404
+
405
+ const fieldErrorMapMap = await Promise.all(fieldValidationPromises)
406
+ return fieldErrorMapMap.flat()
407
+ }
408
+
409
+ validateField = <TField extends DeepKeys<TFormData>>(
410
+ field: TField,
411
+ cause: ValidationCause,
412
+ ) => {
413
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
414
+ const fieldInstance = this.fieldInfo[field]?.instance
415
+ if (!fieldInstance) return []
416
+
417
+ // If the field is not touched (same logic as in validateAllFields)
418
+ if (!fieldInstance.state.meta.isTouched) {
419
+ // Mark it as touched
420
+ fieldInstance.setMeta((prev) => ({ ...prev, isTouched: true }))
421
+ }
422
+
423
+ return fieldInstance.validate(cause)
424
+ }
425
+
373
426
  // TODO: This code is copied from FieldApi, we should refactor to share
374
427
  validateSync = (cause: ValidationCause) => {
375
428
  const validates = getSyncValidatorArray(cause, this.options)
@@ -689,14 +742,15 @@ export class FormApi<
689
742
  : never,
690
743
  opts?: { touch?: boolean },
691
744
  ) => {
692
- return this.setFieldValue(
745
+ this.setFieldValue(
693
746
  field,
694
747
  (prev) => [...(Array.isArray(prev) ? prev : []), value] as any,
695
748
  opts,
696
749
  )
750
+ this.validateField(field, 'change')
697
751
  }
698
752
 
699
- insertFieldValue = <TField extends DeepKeys<TFormData>>(
753
+ insertFieldValue = async <TField extends DeepKeys<TFormData>>(
700
754
  field: TField,
701
755
  index: number,
702
756
  value: DeepValue<TFormData, TField> extends any[]
@@ -713,6 +767,10 @@ export class FormApi<
713
767
  },
714
768
  opts,
715
769
  )
770
+
771
+ // Validate the whole array + all fields that have shifted
772
+ await this.validateField(field, 'change')
773
+ await this.validateArrayFieldsStartingFrom(field, index, 'change')
716
774
  }
717
775
 
718
776
  removeFieldValue = async <TField extends DeepKeys<TFormData>>(
@@ -746,7 +804,9 @@ export class FormApi<
746
804
  fieldsToDelete.forEach((f) => this.deleteField(f as TField))
747
805
  }
748
806
 
749
- await this.validateAllFields('change')
807
+ // Validate the whole array + all fields that have shifted
808
+ await this.validateField(field, 'change')
809
+ await this.validateArrayFieldsStartingFrom(field, index, 'change')
750
810
  }
751
811
 
752
812
  swapFieldValues = <TField extends DeepKeys<TFormData>>(
@@ -759,6 +819,12 @@ export class FormApi<
759
819
  const prev2 = prev[index2]!
760
820
  return setBy(setBy(prev, `${index1}`, prev2), `${index2}`, prev1)
761
821
  })
822
+
823
+ // Validate the whole array
824
+ this.validateField(field, 'change')
825
+ // Validate the swapped fields
826
+ this.validateField(`${field}[${index1}]` as DeepKeys<TFormData>, 'change')
827
+ this.validateField(`${field}[${index2}]` as DeepKeys<TFormData>, 'change')
762
828
  }
763
829
 
764
830
  moveFieldValues = <TField extends DeepKeys<TFormData>>(
@@ -770,6 +836,12 @@ export class FormApi<
770
836
  prev.splice(index2, 0, prev.splice(index1, 1)[0])
771
837
  return prev
772
838
  })
839
+
840
+ // Validate the whole array
841
+ this.validateField(field, 'change')
842
+ // Validate the moved fields
843
+ this.validateField(`${field}[${index1}]` as DeepKeys<TFormData>, 'change')
844
+ this.validateField(`${field}[${index2}]` as DeepKeys<TFormData>, 'change')
773
845
  }
774
846
  }
775
847
 
@@ -107,6 +107,35 @@ describe('field api', () => {
107
107
  expect(field.getValue()).toStrictEqual(['one', 'other'])
108
108
  })
109
109
 
110
+ it('should run onChange validation when pushing an array fields value', async () => {
111
+ const form = new FormApi({
112
+ defaultValues: {
113
+ names: ['test'],
114
+ },
115
+ })
116
+ form.mount()
117
+
118
+ const field = new FieldApi({
119
+ form,
120
+ name: 'names',
121
+ validators: {
122
+ onChange: ({ value }) => {
123
+ if (value.length < 3) {
124
+ return 'At least 3 names are required'
125
+ }
126
+ return
127
+ },
128
+ },
129
+ })
130
+ field.mount()
131
+
132
+ field.pushValue('other')
133
+
134
+ expect(field.getMeta().errors).toStrictEqual([
135
+ 'At least 3 names are required',
136
+ ])
137
+ })
138
+
110
139
  it('should insert a value into an array value correctly', () => {
111
140
  const form = new FormApi({
112
141
  defaultValues: {
@@ -124,6 +153,38 @@ describe('field api', () => {
124
153
  expect(field.getValue()).toStrictEqual(['one', 'other'])
125
154
  })
126
155
 
156
+ it('should run onChange validation when inserting an array fields value', () => {
157
+ const form = new FormApi({
158
+ defaultValues: {
159
+ names: ['test'],
160
+ },
161
+ })
162
+ form.mount()
163
+
164
+ const field = new FieldApi({
165
+ form,
166
+ name: 'names',
167
+ validators: {
168
+ onChange: ({ value }) => {
169
+ if (value.length < 3) {
170
+ return 'At least 3 names are required'
171
+ }
172
+ return
173
+ },
174
+ },
175
+ defaultMeta: {
176
+ isTouched: true,
177
+ },
178
+ })
179
+ field.mount()
180
+
181
+ field.insertValue(1, 'other')
182
+
183
+ expect(field.getMeta().errors).toStrictEqual([
184
+ 'At least 3 names are required',
185
+ ])
186
+ })
187
+
127
188
  it('should remove a value from an array value correctly', () => {
128
189
  const form = new FormApi({
129
190
  defaultValues: {
@@ -141,6 +202,38 @@ describe('field api', () => {
141
202
  expect(field.getValue()).toStrictEqual(['one'])
142
203
  })
143
204
 
205
+ it('should run onChange validation when removing an array fields value', async () => {
206
+ const form = new FormApi({
207
+ defaultValues: {
208
+ names: ['test'],
209
+ },
210
+ })
211
+ form.mount()
212
+
213
+ const field = new FieldApi({
214
+ form,
215
+ name: 'names',
216
+ validators: {
217
+ onChange: ({ value }) => {
218
+ if (value.length < 3) {
219
+ return 'At least 3 names are required'
220
+ }
221
+ return
222
+ },
223
+ },
224
+ defaultMeta: {
225
+ isTouched: true,
226
+ },
227
+ })
228
+ field.mount()
229
+
230
+ await field.removeValue(0)
231
+
232
+ expect(field.getMeta().errors).toStrictEqual([
233
+ 'At least 3 names are required',
234
+ ])
235
+ })
236
+
144
237
  it('should remove a subfield from an array field correctly', async () => {
145
238
  const form = new FormApi({
146
239
  defaultValues: {
@@ -269,6 +362,38 @@ describe('field api', () => {
269
362
  expect(field.getValue()).toStrictEqual(['two', 'one'])
270
363
  })
271
364
 
365
+ it('should run onChange validation when swapping an array fields value', () => {
366
+ const form = new FormApi({
367
+ defaultValues: {
368
+ names: ['test', 'test2'],
369
+ },
370
+ })
371
+ form.mount()
372
+
373
+ const field = new FieldApi({
374
+ form,
375
+ name: 'names',
376
+ validators: {
377
+ onChange: ({ value }) => {
378
+ if (value.length < 3) {
379
+ return 'At least 3 names are required'
380
+ }
381
+ return
382
+ },
383
+ },
384
+ defaultMeta: {
385
+ isTouched: true,
386
+ },
387
+ })
388
+ field.mount()
389
+
390
+ field.swapValues(0, 1)
391
+
392
+ expect(field.getMeta().errors).toStrictEqual([
393
+ 'At least 3 names are required',
394
+ ])
395
+ })
396
+
272
397
  it('should move a value from an array value correctly', () => {
273
398
  const form = new FormApi({
274
399
  defaultValues: {
@@ -286,6 +411,38 @@ describe('field api', () => {
286
411
  expect(field.getValue()).toStrictEqual(['three', 'one', 'two', 'four'])
287
412
  })
288
413
 
414
+ it('should run onChange validation when moving an array fields value', () => {
415
+ const form = new FormApi({
416
+ defaultValues: {
417
+ names: ['test', 'test2'],
418
+ },
419
+ })
420
+ form.mount()
421
+
422
+ const field = new FieldApi({
423
+ form,
424
+ name: 'names',
425
+ validators: {
426
+ onChange: ({ value }) => {
427
+ if (value.length < 3) {
428
+ return 'At least 3 names are required'
429
+ }
430
+ return
431
+ },
432
+ },
433
+ defaultMeta: {
434
+ isTouched: true,
435
+ },
436
+ })
437
+ field.mount()
438
+
439
+ field.moveValue(0, 1)
440
+
441
+ expect(field.getMeta().errors).toStrictEqual([
442
+ 'At least 3 names are required',
443
+ ])
444
+ })
445
+
289
446
  it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => {
290
447
  const form = new FormApi({
291
448
  defaultValues: {