Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/form-core/src/FieldGroupApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TFieldGroupData, any[]>,
>(
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.
*/
Expand Down
49 changes: 41 additions & 8 deletions packages/form-core/src/FormApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1545,6 +1545,23 @@ export class FormApi<
return fieldErrorMapMap.flat()
}

/**
* @private
*/
collectArrayFields = <TField extends DeepKeysOfType<TFormData, any[]>>(
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<TFormData>[]

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.
*/
Expand All @@ -1561,16 +1578,32 @@ export class FormApi<
? Math.max((currentValue as Array<unknown>).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<TFormData>[] = []
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<TFormData>[]
// Validate the fields
const fieldValidationPromises: Promise<ValidationError[]>[] = [] 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 <TField extends DeepKeysOfType<TFormData, any[]>>(
field: TField,
index: number,
cause: ValidationCause,
) => {
const fieldsToValidate = this.collectArrayFields(field, index)

// Validate the fields
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
Expand Down
9 changes: 9 additions & 0 deletions packages/form-core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@ export interface FieldManipulator<TFormData, TSubmitMeta> {
cause: ValidationCause,
) => Promise<unknown[]>

/**
* Validates the children of a specified array in the form using the correct handlers for a given validation type.
*/
validateArrayFields: <TField extends DeepKeysOfType<TFormData, any[]>>(
field: TField,
index: number,
cause: ValidationCause,
) => Promise<unknown[]>

/**
* Validates a specified field in the form using the correct handlers for a given validation type.
*/
Expand Down
90 changes: 90 additions & 0 deletions packages/form-core/tests/FieldGroupApi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: '',
Expand Down
6 changes: 6 additions & 0 deletions packages/form-core/tests/FormApi.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,12 @@ it('should only allow array fields for array-specific methods', () => {
const validate2 = form.validateArrayFieldsStartingFrom<AllKeys>
// @ts-expect-error too wide!
const validate3 = form.validateArrayFieldsStartingFrom<RandomKeys>

const validate4 = form.validateArrayFields<OnlyArrayKeys>
// @ts-expect-error too wide!
const validate5 = form.validateArrayFields<AllKeys>
// @ts-expect-error too wide!
const validate6 = form.validateArrayFields<RandomKeys>
})

it('should infer full field name union for form.resetField parameters', () => {
Expand Down