@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/dist/cjs/FieldApi.cjs +1 -3
- package/dist/cjs/FieldApi.cjs.map +1 -1
- package/dist/cjs/FieldApi.d.cts +1 -1
- package/dist/cjs/FormApi.cjs +44 -3
- package/dist/cjs/FormApi.cjs.map +1 -1
- package/dist/cjs/FormApi.d.cts +3 -1
- package/dist/cjs/utils.cjs +3 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/FieldApi.d.ts +1 -1
- package/dist/esm/FieldApi.js +1 -3
- package/dist/esm/FieldApi.js.map +1 -1
- package/dist/esm/FormApi.d.ts +3 -1
- package/dist/esm/FormApi.js +44 -3
- package/dist/esm/FormApi.js.map +1 -1
- package/dist/esm/utils.js +3 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +1 -1
- package/src/FieldApi.ts +2 -3
- package/src/FormApi.ts +75 -3
- package/src/tests/FieldApi.spec.ts +157 -0
- package/src/tests/FormApi.spec.ts +362 -0
- package/src/utils.ts +3 -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,15 @@ 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')
|
|
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
|
-
|
|
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: {
|