Skip to content

Commit 5657037

Browse files
logaretmclaude
andcommitted
fix: show validation error when field array is empty (#4981)
When all items are removed from a field array, validation errors for the array itself (e.g., min length) were being incorrectly hoisted to a parent path by findHoistedPath. This fix skips hoisting when the error path matches a known field array path, ensuring the error is placed at the correct path. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d8cc52 commit 5657037

3 files changed

Lines changed: 74 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"vee-validate": patch
3+
---
4+
5+
Fix error message not showing when field array is empty (#4981)

packages/vee-validate/src/useForm.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,9 @@ export function useForm<
366366
const results = paths.reduce(
367367
(validation, _path) => {
368368
const expectedPath = _path as Path<TValues>;
369-
const pathState = findPathState(expectedPath) || findHoistedPath(expectedPath);
369+
const isFieldArrayPath = fieldArrays.some(a => toValue(a.path) === expectedPath);
370+
const pathState =
371+
findPathState(expectedPath) || (isFieldArrayPath ? undefined : findHoistedPath(expectedPath));
370372
const messages = formResult.results[expectedPath]?.errors || [];
371373
// This is the real path of the field, because it might've been a hoisted field
372374
const path = (toValue(pathState?.path) || expectedPath) as Path<TValues>;

packages/vee-validate/tests/useFieldArray.spec.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,72 @@ test('array move initializes the array if undefined', async () => {
521521
expect(arr.fields.value).toHaveLength(0);
522522
});
523523

524+
// #4981
525+
test('shows validation error when field array is empty after removing all items', async () => {
526+
let form!: FormContext;
527+
let arr!: FieldArrayContext;
528+
mountWithHoc({
529+
setup() {
530+
form = useForm<any>({
531+
initialValues: {
532+
users: ['one'],
533+
},
534+
validationSchema: z.object({
535+
users: z.array(z.string()).min(1, 'At least one item is required'),
536+
}),
537+
});
538+
539+
arr = useFieldArray('users');
540+
},
541+
template: `
542+
<div></div>
543+
`,
544+
});
545+
546+
await flushPromises();
547+
expect(form.meta.value.valid).toBe(true);
548+
arr.remove(0);
549+
await flushPromises();
550+
expect(form.meta.value.valid).toBe(false);
551+
expect(form.errors.value['users']).toBe('At least one item is required');
552+
});
553+
554+
// #4981
555+
test('shows validation error at correct path when nested field array is empty', async () => {
556+
let form!: FormContext;
557+
let arr!: FieldArrayContext;
558+
mountWithHoc({
559+
setup() {
560+
form = useForm<any>({
561+
initialValues: {
562+
settings: {
563+
items: ['one'],
564+
},
565+
},
566+
validationSchema: z.object({
567+
settings: z.object({
568+
items: z.array(z.string()).min(1, 'At least one item is required'),
569+
}),
570+
}),
571+
});
572+
573+
arr = useFieldArray('settings.items');
574+
},
575+
template: `
576+
<div></div>
577+
`,
578+
});
579+
580+
await flushPromises();
581+
expect(form.meta.value.valid).toBe(true);
582+
arr.remove(0);
583+
await flushPromises();
584+
expect(form.meta.value.valid).toBe(false);
585+
// The error should be at 'settings.items', NOT at 'settings'
586+
expect(form.errors.value['settings.items']).toBe('At least one item is required');
587+
expect(form.errors.value['settings']).toBeUndefined();
588+
});
589+
524590
// #4557
525591
test('errors are available to the newly inserted items', async () => {
526592
let arr!: FieldArrayContext;

0 commit comments

Comments
 (0)