@tanstack/form-core 0.20.2 → 0.21.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/dist/cjs/FieldApi.cjs +6 -7
- package/dist/cjs/FieldApi.cjs.map +1 -1
- package/dist/cjs/FieldApi.d.cts +15 -4
- package/dist/cjs/FormApi.cjs +77 -14
- package/dist/cjs/FormApi.cjs.map +1 -1
- package/dist/cjs/FormApi.d.cts +12 -3
- package/dist/esm/FieldApi.d.ts +15 -4
- package/dist/esm/FieldApi.js +6 -7
- package/dist/esm/FieldApi.js.map +1 -1
- package/dist/esm/FormApi.d.ts +12 -3
- package/dist/esm/FormApi.js +77 -14
- package/dist/esm/FormApi.js.map +1 -1
- package/package.json +1 -1
- package/src/FieldApi.ts +19 -11
- package/src/FormApi.ts +118 -12
- package/src/tests/FieldApi.spec.ts +192 -1
- package/src/tests/FormApi.spec.ts +369 -0
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,39 @@ export class FormApi<
|
|
|
689
742
|
: never,
|
|
690
743
|
opts?: { touch?: boolean },
|
|
691
744
|
) => {
|
|
692
|
-
|
|
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')
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
insertFieldValue = async <TField extends DeepKeys<TFormData>>(
|
|
754
|
+
field: TField,
|
|
755
|
+
index: number,
|
|
756
|
+
value: DeepValue<TFormData, TField> extends any[]
|
|
757
|
+
? DeepValue<TFormData, TField>[number]
|
|
758
|
+
: never,
|
|
759
|
+
opts?: { touch?: boolean },
|
|
760
|
+
) => {
|
|
761
|
+
this.setFieldValue(
|
|
762
|
+
field,
|
|
763
|
+
(prev) => {
|
|
764
|
+
return [
|
|
765
|
+
...(prev as DeepValue<TFormData, TField>[]).slice(0, index),
|
|
766
|
+
value,
|
|
767
|
+
...(prev as DeepValue<TFormData, TField>[]).slice(index),
|
|
768
|
+
] as any
|
|
769
|
+
},
|
|
770
|
+
opts,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
// Validate the whole array + all fields that have shifted
|
|
774
|
+
await this.validateField(field, 'change')
|
|
697
775
|
}
|
|
698
776
|
|
|
699
|
-
|
|
777
|
+
replaceFieldValue = async <TField extends DeepKeys<TFormData>>(
|
|
700
778
|
field: TField,
|
|
701
779
|
index: number,
|
|
702
780
|
value: DeepValue<TFormData, TField> extends any[]
|
|
@@ -713,6 +791,10 @@ export class FormApi<
|
|
|
713
791
|
},
|
|
714
792
|
opts,
|
|
715
793
|
)
|
|
794
|
+
|
|
795
|
+
// Validate the whole array + all fields that have shifted
|
|
796
|
+
await this.validateField(field, 'change')
|
|
797
|
+
await this.validateArrayFieldsStartingFrom(field, index, 'change')
|
|
716
798
|
}
|
|
717
799
|
|
|
718
800
|
removeFieldValue = async <TField extends DeepKeys<TFormData>>(
|
|
@@ -746,30 +828,54 @@ export class FormApi<
|
|
|
746
828
|
fieldsToDelete.forEach((f) => this.deleteField(f as TField))
|
|
747
829
|
}
|
|
748
830
|
|
|
749
|
-
|
|
831
|
+
// Validate the whole array + all fields that have shifted
|
|
832
|
+
await this.validateField(field, 'change')
|
|
833
|
+
await this.validateArrayFieldsStartingFrom(field, index, 'change')
|
|
750
834
|
}
|
|
751
835
|
|
|
752
836
|
swapFieldValues = <TField extends DeepKeys<TFormData>>(
|
|
753
837
|
field: TField,
|
|
754
838
|
index1: number,
|
|
755
839
|
index2: number,
|
|
840
|
+
opts?: { touch?: boolean },
|
|
756
841
|
) => {
|
|
757
|
-
this.setFieldValue(
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
842
|
+
this.setFieldValue(
|
|
843
|
+
field,
|
|
844
|
+
(prev: any) => {
|
|
845
|
+
const prev1 = prev[index1]!
|
|
846
|
+
const prev2 = prev[index2]!
|
|
847
|
+
return setBy(setBy(prev, `${index1}`, prev2), `${index2}`, prev1)
|
|
848
|
+
},
|
|
849
|
+
opts,
|
|
850
|
+
)
|
|
851
|
+
|
|
852
|
+
// Validate the whole array
|
|
853
|
+
this.validateField(field, 'change')
|
|
854
|
+
// Validate the swapped fields
|
|
855
|
+
this.validateField(`${field}[${index1}]` as DeepKeys<TFormData>, 'change')
|
|
856
|
+
this.validateField(`${field}[${index2}]` as DeepKeys<TFormData>, 'change')
|
|
762
857
|
}
|
|
763
858
|
|
|
764
859
|
moveFieldValues = <TField extends DeepKeys<TFormData>>(
|
|
765
860
|
field: TField,
|
|
766
861
|
index1: number,
|
|
767
862
|
index2: number,
|
|
863
|
+
opts?: { touch?: boolean },
|
|
768
864
|
) => {
|
|
769
|
-
this.setFieldValue(
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
865
|
+
this.setFieldValue(
|
|
866
|
+
field,
|
|
867
|
+
(prev: any) => {
|
|
868
|
+
prev.splice(index2, 0, prev.splice(index1, 1)[0])
|
|
869
|
+
return prev
|
|
870
|
+
},
|
|
871
|
+
opts,
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
// Validate the whole array
|
|
875
|
+
this.validateField(field, 'change')
|
|
876
|
+
// Validate the moved fields
|
|
877
|
+
this.validateField(`${field}[${index1}]` as DeepKeys<TFormData>, 'change')
|
|
878
|
+
this.validateField(`${field}[${index2}]` as DeepKeys<TFormData>, 'change')
|
|
773
879
|
}
|
|
774
880
|
}
|
|
775
881
|
|
|
@@ -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: {
|
|
@@ -121,7 +150,73 @@ describe('field api', () => {
|
|
|
121
150
|
|
|
122
151
|
field.insertValue(1, 'other')
|
|
123
152
|
|
|
124
|
-
expect(field.getValue()).toStrictEqual(['one', 'other'])
|
|
153
|
+
expect(field.getValue()).toStrictEqual(['one', 'other', 'two'])
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('should replace a value into an array correctly', () => {
|
|
157
|
+
const form = new FormApi({
|
|
158
|
+
defaultValues: {
|
|
159
|
+
names: ['one', 'two', 'three'],
|
|
160
|
+
},
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
const field = new FieldApi({
|
|
164
|
+
form,
|
|
165
|
+
name: 'names',
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
field.replaceValue(1, 'other')
|
|
169
|
+
|
|
170
|
+
expect(field.getValue()).toStrictEqual(['one', 'other', 'three'])
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('should do nothing when replacing a value into an array at an index that does not exist', () => {
|
|
174
|
+
const form = new FormApi({
|
|
175
|
+
defaultValues: {
|
|
176
|
+
names: ['one', 'two', 'three'],
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const field = new FieldApi({
|
|
181
|
+
form,
|
|
182
|
+
name: 'names',
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
field.replaceValue(10, 'other')
|
|
186
|
+
|
|
187
|
+
expect(field.getValue()).toStrictEqual(['one', 'two', 'three'])
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
it('should run onChange validation when inserting an array fields value', () => {
|
|
191
|
+
const form = new FormApi({
|
|
192
|
+
defaultValues: {
|
|
193
|
+
names: ['test'],
|
|
194
|
+
},
|
|
195
|
+
})
|
|
196
|
+
form.mount()
|
|
197
|
+
|
|
198
|
+
const field = new FieldApi({
|
|
199
|
+
form,
|
|
200
|
+
name: 'names',
|
|
201
|
+
validators: {
|
|
202
|
+
onChange: ({ value }) => {
|
|
203
|
+
if (value.length < 3) {
|
|
204
|
+
return 'At least 3 names are required'
|
|
205
|
+
}
|
|
206
|
+
return
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
defaultMeta: {
|
|
210
|
+
isTouched: true,
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
field.mount()
|
|
214
|
+
|
|
215
|
+
field.insertValue(1, 'other')
|
|
216
|
+
|
|
217
|
+
expect(field.getMeta().errors).toStrictEqual([
|
|
218
|
+
'At least 3 names are required',
|
|
219
|
+
])
|
|
125
220
|
})
|
|
126
221
|
|
|
127
222
|
it('should remove a value from an array value correctly', () => {
|
|
@@ -141,6 +236,38 @@ describe('field api', () => {
|
|
|
141
236
|
expect(field.getValue()).toStrictEqual(['one'])
|
|
142
237
|
})
|
|
143
238
|
|
|
239
|
+
it('should run onChange validation when removing an array fields value', async () => {
|
|
240
|
+
const form = new FormApi({
|
|
241
|
+
defaultValues: {
|
|
242
|
+
names: ['test'],
|
|
243
|
+
},
|
|
244
|
+
})
|
|
245
|
+
form.mount()
|
|
246
|
+
|
|
247
|
+
const field = new FieldApi({
|
|
248
|
+
form,
|
|
249
|
+
name: 'names',
|
|
250
|
+
validators: {
|
|
251
|
+
onChange: ({ value }) => {
|
|
252
|
+
if (value.length < 3) {
|
|
253
|
+
return 'At least 3 names are required'
|
|
254
|
+
}
|
|
255
|
+
return
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
defaultMeta: {
|
|
259
|
+
isTouched: true,
|
|
260
|
+
},
|
|
261
|
+
})
|
|
262
|
+
field.mount()
|
|
263
|
+
|
|
264
|
+
await field.removeValue(0)
|
|
265
|
+
|
|
266
|
+
expect(field.getMeta().errors).toStrictEqual([
|
|
267
|
+
'At least 3 names are required',
|
|
268
|
+
])
|
|
269
|
+
})
|
|
270
|
+
|
|
144
271
|
it('should remove a subfield from an array field correctly', async () => {
|
|
145
272
|
const form = new FormApi({
|
|
146
273
|
defaultValues: {
|
|
@@ -269,6 +396,38 @@ describe('field api', () => {
|
|
|
269
396
|
expect(field.getValue()).toStrictEqual(['two', 'one'])
|
|
270
397
|
})
|
|
271
398
|
|
|
399
|
+
it('should run onChange validation when swapping an array fields value', () => {
|
|
400
|
+
const form = new FormApi({
|
|
401
|
+
defaultValues: {
|
|
402
|
+
names: ['test', 'test2'],
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
form.mount()
|
|
406
|
+
|
|
407
|
+
const field = new FieldApi({
|
|
408
|
+
form,
|
|
409
|
+
name: 'names',
|
|
410
|
+
validators: {
|
|
411
|
+
onChange: ({ value }) => {
|
|
412
|
+
if (value.length < 3) {
|
|
413
|
+
return 'At least 3 names are required'
|
|
414
|
+
}
|
|
415
|
+
return
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
defaultMeta: {
|
|
419
|
+
isTouched: true,
|
|
420
|
+
},
|
|
421
|
+
})
|
|
422
|
+
field.mount()
|
|
423
|
+
|
|
424
|
+
field.swapValues(0, 1)
|
|
425
|
+
|
|
426
|
+
expect(field.getMeta().errors).toStrictEqual([
|
|
427
|
+
'At least 3 names are required',
|
|
428
|
+
])
|
|
429
|
+
})
|
|
430
|
+
|
|
272
431
|
it('should move a value from an array value correctly', () => {
|
|
273
432
|
const form = new FormApi({
|
|
274
433
|
defaultValues: {
|
|
@@ -286,6 +445,38 @@ describe('field api', () => {
|
|
|
286
445
|
expect(field.getValue()).toStrictEqual(['three', 'one', 'two', 'four'])
|
|
287
446
|
})
|
|
288
447
|
|
|
448
|
+
it('should run onChange validation when moving an array fields value', () => {
|
|
449
|
+
const form = new FormApi({
|
|
450
|
+
defaultValues: {
|
|
451
|
+
names: ['test', 'test2'],
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
form.mount()
|
|
455
|
+
|
|
456
|
+
const field = new FieldApi({
|
|
457
|
+
form,
|
|
458
|
+
name: 'names',
|
|
459
|
+
validators: {
|
|
460
|
+
onChange: ({ value }) => {
|
|
461
|
+
if (value.length < 3) {
|
|
462
|
+
return 'At least 3 names are required'
|
|
463
|
+
}
|
|
464
|
+
return
|
|
465
|
+
},
|
|
466
|
+
},
|
|
467
|
+
defaultMeta: {
|
|
468
|
+
isTouched: true,
|
|
469
|
+
},
|
|
470
|
+
})
|
|
471
|
+
field.mount()
|
|
472
|
+
|
|
473
|
+
field.moveValue(0, 1)
|
|
474
|
+
|
|
475
|
+
expect(field.getMeta().errors).toStrictEqual([
|
|
476
|
+
'At least 3 names are required',
|
|
477
|
+
])
|
|
478
|
+
})
|
|
479
|
+
|
|
289
480
|
it('should not throw errors when no meta info is stored on a field and a form re-renders', async () => {
|
|
290
481
|
const form = new FormApi({
|
|
291
482
|
defaultValues: {
|