Skip to content

Commit 2b5f759

Browse files
logaretmclaude
andcommitted
fix: useField with object values now validates correctly (#5088)
When a schema (e.g., yup) reports validation errors at nested sub-paths (like 'someObject.id') but the field is registered at a parent path (like 'someObject'), the errors were stored in extraErrorsBag and never cleaned up. This caused form.meta.valid to remain false even after resetForm with valid values. Two fixes: 1. In validateSchema callback, also clean up extraErrorsBag entries for the original error path when it gets hoisted to a parent field path. 2. In resetForm, clear extraErrorsBag before setting new errors to ensure stale nested-path errors don't persist. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d8cc52 commit 2b5f759

3 files changed

Lines changed: 85 additions & 0 deletions

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 useField with object values not triggering validation (#5088)

packages/vee-validate/src/useForm.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,13 @@ export function useForm<
386386
delete extraErrorsBag.value[path];
387387
}
388388

389+
// Also clean up extra errors for the expected path when it was hoisted to a different path
390+
// This handles cases where schema reports errors at nested sub-paths (e.g., 'someObject.id')
391+
// but the field is registered at a parent path (e.g., 'someObject')
392+
if (pathState && expectedPath !== path && extraErrorsBag.value[expectedPath]) {
393+
delete extraErrorsBag.value[expectedPath];
394+
}
395+
389396
// field not rendered
390397
if (!pathState) {
391398
setFieldError(path, messages);
@@ -811,6 +818,8 @@ export function useForm<
811818
});
812819

813820
opts?.force ? forceSetValues(newValues, false) : setValues(newValues, false);
821+
// Clear extra errors that were set for paths without a pathState (e.g., nested paths of hoisted fields)
822+
extraErrorsBag.value = {};
814823
setErrors(resetState?.errors || {});
815824
submitCount.value = resetState?.submitCount || 0;
816825
nextTick(() => {

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

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '@/vee-validate';
1111
import { mountWithHoc, setValue, flushPromises, dispatchEvent } from './helpers';
1212
import * as z from 'zod';
13+
import * as yup from 'yup';
1314
import { onMounted, ref, Ref } from 'vue';
1415
import { ModelComp, CustomModelComp } from './helpers/ModelComp';
1516

@@ -1489,4 +1490,74 @@ describe('useForm()', () => {
14891490
form.setValues({ file: f2 });
14901491
expect(form.values.file).toEqual(f2);
14911492
});
1493+
1494+
// #5088 - extraErrorsBag entries for hoisted/nested paths should be cleaned up after validation
1495+
test('useField with object values validates correctly after resetForm with delayed field rendering', async () => {
1496+
let form!: FormContext<any>;
1497+
let fieldCtx!: FieldContext<any>;
1498+
const showField = ref(false);
1499+
1500+
mountWithHoc({
1501+
setup() {
1502+
form = useForm({
1503+
validationSchema: yup.object({
1504+
someObject: yup
1505+
.object({
1506+
id: yup.number().required(),
1507+
title: yup.string(),
1508+
})
1509+
.nullable(),
1510+
}),
1511+
});
1512+
1513+
return {
1514+
showField,
1515+
};
1516+
},
1517+
template: `
1518+
<div>
1519+
<Child v-if="showField" />
1520+
</div>
1521+
`,
1522+
components: {
1523+
Child: {
1524+
setup() {
1525+
const field = useField('someObject');
1526+
fieldCtx = field;
1527+
return { value: field.value };
1528+
},
1529+
template: '<span id="fieldValue">{{ value }}</span>',
1530+
},
1531+
},
1532+
});
1533+
1534+
await flushPromises();
1535+
1536+
// At this point, the initial silent validation has run on empty form values.
1537+
// Yup reports errors at nested paths like 'someObject.id' (not 'someObject').
1538+
// These errors go into extraErrorsBag since no pathStates exist yet.
1539+
1540+
// Reset the form with valid values and immediately show the field
1541+
form.resetForm({
1542+
values: {
1543+
someObject: {
1544+
id: 1,
1545+
title: 'Lorem Ipsum',
1546+
},
1547+
},
1548+
});
1549+
showField.value = true;
1550+
await flushPromises();
1551+
1552+
// The form values should be set correctly
1553+
expect(form.values.someObject).toEqual({ id: 1, title: 'Lorem Ipsum' });
1554+
// The field should have the correct value
1555+
expect(fieldCtx.value.value).toEqual({ id: 1, title: 'Lorem Ipsum' });
1556+
// The field meta should report valid
1557+
expect(fieldCtx.meta.valid).toBe(true);
1558+
// The form meta should report valid since the values satisfy the schema
1559+
expect(form.meta.value.valid).toBe(true);
1560+
// No errors should be reported
1561+
expect(form.errors.value).toEqual({});
1562+
});
14921563
});

0 commit comments

Comments
 (0)