Skip to content

Commit 9d8a245

Browse files
logaretmclaude
andcommitted
fix: setFieldValue respects per-field validateOn configuration (#5050)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7d8cc52 commit 9d8a245

File tree

4 files changed

+84
-2
lines changed

4 files changed

+84
-2
lines changed
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+
setFieldValue and setValue now respect Field validateOnChange/validateOnInput props (#5050)

packages/vee-validate/src/useField.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ function _useField<TValue = unknown>(
207207
return validateValidStateOnly();
208208
}
209209

210+
if (opts?.mode === 'validated-only' && !meta.validated) {
211+
return validateValidStateOnly();
212+
}
213+
210214
return validateWithStateMutation();
211215
}
212216

packages/vee-validate/src/useForm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -669,7 +669,7 @@ export function useForm<
669669

670670
setInPath(formValues, path, clonedValue);
671671
if (shouldValidate) {
672-
validateField(path);
672+
validateField(path, { mode: 'validated-only' });
673673
}
674674
}
675675

@@ -892,7 +892,7 @@ export function useForm<
892892
opts?: Partial<ValidationOptions>,
893893
): Promise<ValidationResult<TOutput[TPath]>> {
894894
const state = findPathState(path);
895-
if (state && opts?.mode !== 'silent') {
895+
if (state && opts?.mode !== 'silent' && opts?.mode !== 'validated-only') {
896896
state.validated = true;
897897
}
898898

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

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3241,3 +3241,76 @@ test('handles onSubmit with generic object from zod schema', async () => {
32413241
expect.anything(),
32423242
);
32433243
});
3244+
3245+
// #5050
3246+
test('setFieldValue from Form slot should not trigger validation when Field validateOnChange is false', async () => {
3247+
const REQUIRED_MSG = 'This field is required';
3248+
defineRule('required_5050', (value: unknown) => {
3249+
if (!value) {
3250+
return REQUIRED_MSG;
3251+
}
3252+
return true;
3253+
});
3254+
3255+
const wrapper = mountWithHoc({
3256+
template: `
3257+
<VForm v-slot="{ setFieldValue, errors }">
3258+
<Field name="field" rules="required_5050" :validateOnChange="false" :validateOnInput="false" :validateOnBlur="false" :validateOnModelUpdate="false" />
3259+
<span id="error">{{ errors.field }}</span>
3260+
<button type="button" @click="setFieldValue('field', '')">Set empty</button>
3261+
</VForm>
3262+
`,
3263+
});
3264+
3265+
await flushPromises();
3266+
const error = wrapper.$el.querySelector('#error');
3267+
expect(error.textContent).toBe('');
3268+
3269+
// Click the button to set the field value to empty string via setFieldValue
3270+
wrapper.$el.querySelector('button').click();
3271+
await flushPromises();
3272+
3273+
// Should NOT show validation error because validateOnChange/Input/Blur/ModelUpdate are all false
3274+
expect(error.textContent).toBe('');
3275+
});
3276+
3277+
// #5050
3278+
test('setFieldValue should still trigger validation on previously validated fields', async () => {
3279+
const REQUIRED_MSG = 'This field is required';
3280+
defineRule('required_5050b', (value: unknown) => {
3281+
if (!value) {
3282+
return REQUIRED_MSG;
3283+
}
3284+
return true;
3285+
});
3286+
3287+
const wrapper = mountWithHoc({
3288+
template: `
3289+
<VForm v-slot="{ setFieldValue, errors }" @submit="() => {}">
3290+
<Field name="field" rules="required_5050b" :validateOnChange="false" :validateOnInput="false" :validateOnBlur="false" :validateOnModelUpdate="false" />
3291+
<span id="error">{{ errors.field }}</span>
3292+
<button id="submit" type="submit">Submit</button>
3293+
<button id="setEmpty" type="button" @click="setFieldValue('field', '')">Set empty</button>
3294+
</VForm>
3295+
`,
3296+
});
3297+
3298+
await flushPromises();
3299+
const error = wrapper.$el.querySelector('#error');
3300+
expect(error.textContent).toBe('');
3301+
3302+
// Submit the form to trigger validation (marks fields as validated)
3303+
wrapper.$el.querySelector('#submit').click();
3304+
await flushPromises();
3305+
3306+
// After submit, the field should show the error since it's empty and required
3307+
expect(error.textContent).toBe(REQUIRED_MSG);
3308+
3309+
// Set value to empty again via setFieldValue
3310+
wrapper.$el.querySelector('#setEmpty').click();
3311+
await flushPromises();
3312+
3313+
// After setFieldValue on a previously validated field, validation should still update
3314+
// The field is already validated so setFieldValue should re-validate
3315+
expect(error.textContent).toBe(REQUIRED_MSG);
3316+
});

0 commit comments

Comments
 (0)