diff --git a/.changeset/fix-5088-object-field-validation.md b/.changeset/fix-5088-object-field-validation.md new file mode 100644 index 000000000..a497cb9ed --- /dev/null +++ b/.changeset/fix-5088-object-field-validation.md @@ -0,0 +1,5 @@ +--- +"vee-validate": patch +--- + +Fix useField with object values not triggering validation (#5088) diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts index f5e7329b8..558ff71bb 100644 --- a/packages/vee-validate/src/useForm.ts +++ b/packages/vee-validate/src/useForm.ts @@ -386,6 +386,13 @@ export function useForm< delete extraErrorsBag.value[path]; } + // Also clean up extra errors for the expected path when it was hoisted to a different path + // This handles cases where schema reports errors at nested sub-paths (e.g., 'someObject.id') + // but the field is registered at a parent path (e.g., 'someObject') + if (pathState && expectedPath !== path && extraErrorsBag.value[expectedPath]) { + delete extraErrorsBag.value[expectedPath]; + } + // field not rendered if (!pathState) { setFieldError(path, messages); @@ -811,6 +818,8 @@ export function useForm< }); opts?.force ? forceSetValues(newValues, false) : setValues(newValues, false); + // Clear extra errors that were set for paths without a pathState (e.g., nested paths of hoisted fields) + extraErrorsBag.value = {}; setErrors(resetState?.errors || {}); submitCount.value = resetState?.submitCount || 0; nextTick(() => { diff --git a/packages/vee-validate/tests/useForm.spec.ts b/packages/vee-validate/tests/useForm.spec.ts index 09671b183..65306b403 100644 --- a/packages/vee-validate/tests/useForm.spec.ts +++ b/packages/vee-validate/tests/useForm.spec.ts @@ -10,6 +10,7 @@ import { } from '@/vee-validate'; import { mountWithHoc, setValue, flushPromises, dispatchEvent } from './helpers'; import * as z from 'zod'; +import * as yup from 'yup'; import { onMounted, ref, Ref } from 'vue'; import { ModelComp, CustomModelComp } from './helpers/ModelComp'; @@ -1489,4 +1490,74 @@ describe('useForm()', () => { form.setValues({ file: f2 }); expect(form.values.file).toEqual(f2); }); + + // #5088 - extraErrorsBag entries for hoisted/nested paths should be cleaned up after validation + test('useField with object values validates correctly after resetForm with delayed field rendering', async () => { + let form!: FormContext; + let fieldCtx!: FieldContext; + const showField = ref(false); + + mountWithHoc({ + setup() { + form = useForm({ + validationSchema: yup.object({ + someObject: yup + .object({ + id: yup.number().required(), + title: yup.string(), + }) + .nullable(), + }), + }); + + return { + showField, + }; + }, + template: ` +
+ +
+ `, + components: { + Child: { + setup() { + const field = useField('someObject'); + fieldCtx = field; + return { value: field.value }; + }, + template: '{{ value }}', + }, + }, + }); + + await flushPromises(); + + // At this point, the initial silent validation has run on empty form values. + // Yup reports errors at nested paths like 'someObject.id' (not 'someObject'). + // These errors go into extraErrorsBag since no pathStates exist yet. + + // Reset the form with valid values and immediately show the field + form.resetForm({ + values: { + someObject: { + id: 1, + title: 'Lorem Ipsum', + }, + }, + }); + showField.value = true; + await flushPromises(); + + // The form values should be set correctly + expect(form.values.someObject).toEqual({ id: 1, title: 'Lorem Ipsum' }); + // The field should have the correct value + expect(fieldCtx.value.value).toEqual({ id: 1, title: 'Lorem Ipsum' }); + // The field meta should report valid + expect(fieldCtx.meta.valid).toBe(true); + // The form meta should report valid since the values satisfy the schema + expect(form.meta.value.valid).toBe(true); + // No errors should be reported + expect(form.errors.value).toEqual({}); + }); });