Skip to content

Commit 8c7fa1d

Browse files
committed
feat(form-core): add validateArrayFields for validating nested fields
1 parent 6a5e1c1 commit 8c7fa1d

File tree

5 files changed

+163
-8
lines changed

5 files changed

+163
-8
lines changed

packages/form-core/src/FieldGroupApi.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,23 @@ export class FieldGroupApi<
326326
)
327327
}
328328

329+
/**
330+
* Validates the children of a specified array in the form using the correct handlers for a given validation type.
331+
*/
332+
validateArrayFields = async <
333+
TField extends DeepKeysOfType<TFieldGroupData, any[]>,
334+
>(
335+
field: TField,
336+
index: number,
337+
cause: ValidationCause,
338+
) => {
339+
return this.form.validateArrayFields(
340+
this.getFormFieldName(field),
341+
index,
342+
cause,
343+
)
344+
}
345+
329346
/**
330347
* Validates a specified field in the form using the correct handlers for a given validation type.
331348
*/

packages/form-core/src/FormApi.ts

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,6 +1545,23 @@ export class FormApi<
15451545
return fieldErrorMapMap.flat()
15461546
}
15471547

1548+
/**
1549+
* @private
1550+
*/
1551+
collectArrayFields = <TField extends DeepKeysOfType<TFormData, any[]>>(
1552+
field: TField,
1553+
index: number,
1554+
) => {
1555+
const fieldKeysToCollect = [`${field}[${index}]`]
1556+
1557+
// We also have to include all fields that are nested in the array fields
1558+
const fieldsToCollect = Object.keys(this.fieldInfo).filter((fieldKey) =>
1559+
fieldKeysToCollect.some((key) => fieldKey.startsWith(key)),
1560+
) as DeepKeys<TFormData>[]
1561+
1562+
return fieldsToCollect
1563+
}
1564+
15481565
/**
15491566
* 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.
15501567
*/
@@ -1561,16 +1578,32 @@ export class FormApi<
15611578
? Math.max((currentValue as Array<unknown>).length - 1, 0)
15621579
: null
15631580

1564-
// We have to validate all fields that have shifted (at least the current field)
1565-
const fieldKeysToValidate = [`${field}[${index}]`]
1566-
for (let i = index + 1; i <= (lastIndex ?? 0); i++) {
1567-
fieldKeysToValidate.push(`${field}[${i}]`)
1581+
const fieldsToValidate: DeepKeys<TFormData>[] = []
1582+
for (let i = index; i <= (lastIndex ?? 0); i++) {
1583+
const collectedFields = this.collectArrayFields(field, i)
1584+
fieldsToValidate.push(...collectedFields)
15681585
}
15691586

1570-
// We also have to include all fields that are nested in the shifted fields
1571-
const fieldsToValidate = Object.keys(this.fieldInfo).filter((fieldKey) =>
1572-
fieldKeysToValidate.some((key) => fieldKey.startsWith(key)),
1573-
) as DeepKeys<TFormData>[]
1587+
// Validate the fields
1588+
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any
1589+
batch(() => {
1590+
fieldsToValidate.forEach((nestedField) => {
1591+
fieldValidationPromises.push(
1592+
Promise.resolve().then(() => this.validateField(nestedField, cause)),
1593+
)
1594+
})
1595+
})
1596+
1597+
const fieldErrorMapMap = await Promise.all(fieldValidationPromises)
1598+
return fieldErrorMapMap.flat()
1599+
}
1600+
1601+
validateArrayFields = async <TField extends DeepKeysOfType<TFormData, any[]>>(
1602+
field: TField,
1603+
index: number,
1604+
cause: ValidationCause,
1605+
) => {
1606+
const fieldsToValidate = this.collectArrayFields(field, index)
15741607

15751608
// Validate the fields
15761609
const fieldValidationPromises: Promise<ValidationError[]>[] = [] as any

packages/form-core/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,15 @@ export interface FieldManipulator<TFormData, TSubmitMeta> {
165165
cause: ValidationCause,
166166
) => Promise<unknown[]>
167167

168+
/**
169+
* Validates the children of a specified array in the form using the correct handlers for a given validation type.
170+
*/
171+
validateArrayFields: <TField extends DeepKeysOfType<TFormData, any[]>>(
172+
field: TField,
173+
index: number,
174+
cause: ValidationCause,
175+
) => Promise<unknown[]>
176+
168177
/**
169178
* Validates a specified field in the form using the correct handlers for a given validation type.
170179
*/

packages/form-core/tests/FieldGroupApi.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,96 @@ describe('field group api', () => {
213213
expect(field3.state.meta.errors).toEqual(['Field 3'])
214214
})
215215

216+
it('should forward validateArrayFields to form', async () => {
217+
vi.useFakeTimers()
218+
219+
const defaultValues = {
220+
people: {
221+
names: [
222+
{
223+
firstName: 'First one',
224+
lastName: 'Last one',
225+
},
226+
{
227+
firstName: 'Second one',
228+
lastName: 'Last two',
229+
},
230+
],
231+
},
232+
}
233+
234+
const form = new FormApi({
235+
defaultValues,
236+
})
237+
form.mount()
238+
239+
const field1FirstName = new FieldApi({
240+
form,
241+
name: 'people.names[0].firstName',
242+
validators: {
243+
onChange: () => 'Field 1 - First Name',
244+
},
245+
})
246+
247+
const field1LastName = new FieldApi({
248+
form,
249+
name: 'people.names[0].lastName',
250+
validators: {
251+
onChange: () => 'Field 1 - Last Name',
252+
},
253+
})
254+
255+
const field2FirstName = new FieldApi({
256+
form,
257+
name: 'people.names[1].firstName',
258+
validators: {
259+
onChange: () => 'Field 2 - First Name',
260+
},
261+
})
262+
263+
const field2LastName = new FieldApi({
264+
form,
265+
name: 'people.names[1].lastName',
266+
validators: {
267+
onChange: () => 'Field 2 - Last Name',
268+
},
269+
})
270+
271+
field1FirstName.mount()
272+
field1LastName.mount()
273+
field2FirstName.mount()
274+
field2LastName.mount()
275+
276+
const fieldGroup = new FieldGroupApi({
277+
form,
278+
defaultValues: {
279+
names: [
280+
{
281+
firstName: '',
282+
lastName: '',
283+
},
284+
{
285+
firstName: '',
286+
lastName: '',
287+
},
288+
],
289+
},
290+
fields: 'people',
291+
})
292+
293+
fieldGroup.mount()
294+
295+
fieldGroup.validateArrayFields('names', 0, 'change')
296+
297+
await vi.runAllTimersAsync()
298+
299+
expect(field1FirstName.state.meta.errors).toEqual(['Field 1 - First Name'])
300+
expect(field1LastName.state.meta.errors).toEqual(['Field 1 - Last Name'])
301+
302+
expect(field2FirstName.state.meta.errors).toEqual([])
303+
expect(field2LastName.state.meta.errors).toEqual([])
304+
})
305+
216306
it('should get the right field value from the nested field', () => {
217307
const defaultValues: FormValues = {
218308
name: '',

packages/form-core/tests/FormApi.test-d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,12 @@ it('should only allow array fields for array-specific methods', () => {
281281
const validate2 = form.validateArrayFieldsStartingFrom<AllKeys>
282282
// @ts-expect-error too wide!
283283
const validate3 = form.validateArrayFieldsStartingFrom<RandomKeys>
284+
285+
const validate4 = form.validateArrayFields<OnlyArrayKeys>
286+
// @ts-expect-error too wide!
287+
const validate5 = form.validateArrayFields<AllKeys>
288+
// @ts-expect-error too wide!
289+
const validate6 = form.validateArrayFields<RandomKeys>
284290
})
285291

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

0 commit comments

Comments
 (0)