diff --git a/packages/form-core/src/FieldGroupApi.ts b/packages/form-core/src/FieldGroupApi.ts index e69a29fc9..8babbd099 100644 --- a/packages/form-core/src/FieldGroupApi.ts +++ b/packages/form-core/src/FieldGroupApi.ts @@ -326,6 +326,23 @@ export class FieldGroupApi< ) } + /** + * Validates the children of a specified array in the form using the correct handlers for a given validation type. + */ + validateArrayFields = async < + TField extends DeepKeysOfType, + >( + field: TField, + index: number, + cause: ValidationCause, + ) => { + return this.form.validateArrayFields( + this.getFormFieldName(field), + index, + cause, + ) + } + /** * Validates a specified field in the form using the correct handlers for a given validation type. */ diff --git a/packages/form-core/src/FormApi.ts b/packages/form-core/src/FormApi.ts index 319b98531..e6a3c9128 100644 --- a/packages/form-core/src/FormApi.ts +++ b/packages/form-core/src/FormApi.ts @@ -1545,6 +1545,23 @@ export class FormApi< return fieldErrorMapMap.flat() } + /** + * @private + */ + collectArrayFields = >( + field: TField, + index: number, + ) => { + const fieldKeysToCollect = [`${field}[${index}]`] + + // We also have to include all fields that are nested in the array fields + const fieldsToCollect = Object.keys(this.fieldInfo).filter((fieldKey) => + fieldKeysToCollect.some((key) => fieldKey.startsWith(key)), + ) as DeepKeys[] + + return fieldsToCollect + } + /** * Validates the children of a specified array in the form starting from a given index until the end using the correct handlers for a given validation type. */ @@ -1561,16 +1578,32 @@ export class FormApi< ? Math.max((currentValue as Array).length - 1, 0) : null - // We have to validate all fields that have shifted (at least the current field) - const fieldKeysToValidate = [`${field}[${index}]`] - for (let i = index + 1; i <= (lastIndex ?? 0); i++) { - fieldKeysToValidate.push(`${field}[${i}]`) + const fieldsToValidate: DeepKeys[] = [] + for (let i = index; i <= (lastIndex ?? 0); i++) { + const collectedFields = this.collectArrayFields(field, i) + fieldsToValidate.push(...collectedFields) } - // We also have to include all fields that are nested in the shifted fields - const fieldsToValidate = Object.keys(this.fieldInfo).filter((fieldKey) => - fieldKeysToValidate.some((key) => fieldKey.startsWith(key)), - ) as DeepKeys[] + // Validate the fields + const fieldValidationPromises: Promise[] = [] as any + batch(() => { + fieldsToValidate.forEach((nestedField) => { + fieldValidationPromises.push( + Promise.resolve().then(() => this.validateField(nestedField, cause)), + ) + }) + }) + + const fieldErrorMapMap = await Promise.all(fieldValidationPromises) + return fieldErrorMapMap.flat() + } + + validateArrayFields = async >( + field: TField, + index: number, + cause: ValidationCause, + ) => { + const fieldsToValidate = this.collectArrayFields(field, index) // Validate the fields const fieldValidationPromises: Promise[] = [] as any diff --git a/packages/form-core/src/types.ts b/packages/form-core/src/types.ts index ff5824283..f17fa7b81 100644 --- a/packages/form-core/src/types.ts +++ b/packages/form-core/src/types.ts @@ -165,6 +165,15 @@ export interface FieldManipulator { cause: ValidationCause, ) => Promise + /** + * Validates the children of a specified array in the form using the correct handlers for a given validation type. + */ + validateArrayFields: >( + field: TField, + index: number, + cause: ValidationCause, + ) => Promise + /** * Validates a specified field in the form using the correct handlers for a given validation type. */ diff --git a/packages/form-core/tests/FieldGroupApi.spec.ts b/packages/form-core/tests/FieldGroupApi.spec.ts index 2ff20790d..9dfeb17a0 100644 --- a/packages/form-core/tests/FieldGroupApi.spec.ts +++ b/packages/form-core/tests/FieldGroupApi.spec.ts @@ -213,6 +213,96 @@ describe('field group api', () => { expect(field3.state.meta.errors).toEqual(['Field 3']) }) + it('should forward validateArrayFields to form', async () => { + vi.useFakeTimers() + + const defaultValues = { + people: { + names: [ + { + firstName: 'First one', + lastName: 'Last one', + }, + { + firstName: 'Second one', + lastName: 'Last two', + }, + ], + }, + } + + const form = new FormApi({ + defaultValues, + }) + form.mount() + + const field1FirstName = new FieldApi({ + form, + name: 'people.names[0].firstName', + validators: { + onChange: () => 'Field 1 - First Name', + }, + }) + + const field1LastName = new FieldApi({ + form, + name: 'people.names[0].lastName', + validators: { + onChange: () => 'Field 1 - Last Name', + }, + }) + + const field2FirstName = new FieldApi({ + form, + name: 'people.names[1].firstName', + validators: { + onChange: () => 'Field 2 - First Name', + }, + }) + + const field2LastName = new FieldApi({ + form, + name: 'people.names[1].lastName', + validators: { + onChange: () => 'Field 2 - Last Name', + }, + }) + + field1FirstName.mount() + field1LastName.mount() + field2FirstName.mount() + field2LastName.mount() + + const fieldGroup = new FieldGroupApi({ + form, + defaultValues: { + names: [ + { + firstName: '', + lastName: '', + }, + { + firstName: '', + lastName: '', + }, + ], + }, + fields: 'people', + }) + + fieldGroup.mount() + + fieldGroup.validateArrayFields('names', 0, 'change') + + await vi.runAllTimersAsync() + + expect(field1FirstName.state.meta.errors).toEqual(['Field 1 - First Name']) + expect(field1LastName.state.meta.errors).toEqual(['Field 1 - Last Name']) + + expect(field2FirstName.state.meta.errors).toEqual([]) + expect(field2LastName.state.meta.errors).toEqual([]) + }) + it('should get the right field value from the nested field', () => { const defaultValues: FormValues = { name: '', diff --git a/packages/form-core/tests/FormApi.test-d.ts b/packages/form-core/tests/FormApi.test-d.ts index 1d2554a6e..7bc2ade20 100644 --- a/packages/form-core/tests/FormApi.test-d.ts +++ b/packages/form-core/tests/FormApi.test-d.ts @@ -281,6 +281,12 @@ it('should only allow array fields for array-specific methods', () => { const validate2 = form.validateArrayFieldsStartingFrom // @ts-expect-error too wide! const validate3 = form.validateArrayFieldsStartingFrom + + const validate4 = form.validateArrayFields + // @ts-expect-error too wide! + const validate5 = form.validateArrayFields + // @ts-expect-error too wide! + const validate6 = form.validateArrayFields }) it('should infer full field name union for form.resetField parameters', () => {