diff --git a/docs/content/guide/components/validation.md b/docs/content/guide/components/validation.md
index 18a60bda1..8d9dc3ce8 100644
--- a/docs/content/guide/components/validation.md
+++ b/docs/content/guide/components/validation.md
@@ -211,6 +211,63 @@ There is an official integration available for [Zod validation](https://github.c
+### Reactive Form Schema
+
+You can have reactive form schemas using `computed` if you are looking to create dynamic schemas using either `yup` or a validation object.
+
+```js
+import * as yup from 'yup';
+
+export default {
+ data: () => ({
+ min: 6,
+ }),
+ computed: {
+ schema() {
+ return yup.object({
+ password: yup.string().min(this.min),
+ });
+ },
+ },
+};
+```
+
+```vue
+
+
+
+
+
+```
+
+When the validation schema changes, only the fields that were validated at least once will be re-validated, the other fields won't be validated to avoid aggressive validation behavior.
+
## Validation Behavior
By default vee-validate runs validation in these scenarios:
diff --git a/docs/content/guide/composition-api/validation.md b/docs/content/guide/composition-api/validation.md
index a956fdd56..d9f239410 100644
--- a/docs/content/guide/composition-api/validation.md
+++ b/docs/content/guide/composition-api/validation.md
@@ -212,6 +212,48 @@ There is an official integration available for [Zod validation](https://github.c
+### Reactive Form Schema
+
+You can have reactive form schemas using `computed` if you are looking to create dynamic schemas using either `yup` or a validation object.
+
+```vue
+
+
+ {{ passwordError }}
+
+
+
+```
+
+When the validation schema changes, only the fields that were validated at least once will be re-validated, the other fields won't be validated to avoid aggressive validation behavior.
+
## Validation Behavior
diff --git a/packages/vee-validate/src/Form.ts b/packages/vee-validate/src/Form.ts
index 3dfd78f3f..a7ab709d1 100644
--- a/packages/vee-validate/src/Form.ts
+++ b/packages/vee-validate/src/Form.ts
@@ -38,6 +38,8 @@ export const Form = defineComponent({
},
setup(props, ctx) {
const initialValues = toRef(props, 'initialValues');
+ const validationSchema = toRef(props, 'validationSchema');
+
const {
errors,
values,
@@ -57,7 +59,7 @@ export const Form = defineComponent({
setFieldTouched,
setTouched,
} = useForm({
- validationSchema: props.validationSchema,
+ validationSchema: validationSchema.value ? validationSchema : undefined,
initialValues,
initialErrors: props.initialErrors,
initialTouched: props.initialTouched,
diff --git a/packages/vee-validate/src/types.ts b/packages/vee-validate/src/types.ts
index f0d43bdf6..3cb1178e6 100644
--- a/packages/vee-validate/src/types.ts
+++ b/packages/vee-validate/src/types.ts
@@ -25,6 +25,7 @@ export interface FieldMeta {
touched: boolean;
dirty: boolean;
valid: boolean;
+ validated: boolean;
pending: boolean;
initialValue?: TValue;
}
@@ -107,14 +108,20 @@ export type SubmissionHandler = Record
) => unknown;
+/**
+ * validated-only: only mutate the previously validated fields
+ * silent: do not mutate any field
+ * force: validate all fields and mutate their state
+ */
+export type SchemaValidationMode = 'validated-only' | 'silent' | 'force';
export interface FormContext = Record> extends FormActions {
register(field: PrivateFieldComposite): void;
unregister(field: PrivateFieldComposite): void;
values: TValues;
fieldsById: ComputedRef>;
submitCount: Ref;
- schema?: Record> | SchemaOf;
- validateSchema?: (shouldMutate?: boolean) => Promise>;
+ schema?: MaybeRef> | SchemaOf>;
+ validateSchema?: (mode: SchemaValidationMode) => Promise>;
validate(): Promise>;
validateField(field: keyof TValues): Promise;
errorBag: Ref>;
diff --git a/packages/vee-validate/src/useField.ts b/packages/vee-validate/src/useField.ts
index 0c0488362..532ffe0ec 100644
--- a/packages/vee-validate/src/useField.ts
+++ b/packages/vee-validate/src/useField.ts
@@ -102,7 +102,7 @@ export function useField(
const normalizedRules = computed(() => {
let rulesValue = unref(rules);
- const schema = form?.schema;
+ const schema = unref(form?.schema);
if (schema && !isYupValidator(schema)) {
rulesValue = extractRuleFromSchema(schema, unref(name)) || rulesValue;
}
@@ -117,6 +117,7 @@ export function useField(
async function validateWithStateMutation(): Promise {
meta.pending = true;
let result: ValidationResult;
+ meta.validated = true;
if (!form || !form.validateSchema) {
result = await validateValue(value.value, normalizedRules.value, {
name: unref(label) || unref(name),
@@ -124,7 +125,7 @@ export function useField(
bails,
});
} else {
- result = (await form.validateSchema())[unref(name)] ?? { valid: true, errors: [] };
+ result = (await form.validateSchema('validated-only'))[unref(name)] ?? { valid: true, errors: [] };
}
meta.pending = false;
@@ -141,7 +142,7 @@ export function useField(
bails,
});
} else {
- result = (await form.validateSchema(false))?.[unref(name)] ?? { valid: true, errors: [] };
+ result = (await form.validateSchema('silent'))?.[unref(name)] ?? { valid: true, errors: [] };
}
meta.valid = result.valid;
@@ -392,6 +393,7 @@ function useValidationState({
setErrors(state?.errors || []);
meta.touched = state?.touched ?? false;
meta.pending = false;
+ meta.validated = false;
}
return {
@@ -416,6 +418,7 @@ function useMeta(initialValue: MaybeRef, currentValue: Ref unref(initialValue) as TValue | undefined),
dirty: computed(() => {
return !isEqual(currentValue.value, unref(initialValue));
diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts
index bc28cb3cb..04e1f7a29 100644
--- a/packages/vee-validate/src/useForm.ts
+++ b/packages/vee-validate/src/useForm.ts
@@ -16,6 +16,7 @@ import {
PublicFormContext,
FormErrors,
FormErrorBag,
+ SchemaValidationMode,
} from './types';
import {
applyFieldMutation,
@@ -29,7 +30,9 @@ import {
import { FormErrorsSymbol, FormContextSymbol, FormInitialValuesSymbol } from './symbols';
interface FormOptions> {
- validationSchema?: Record> | SchemaOf;
+ validationSchema?: MaybeRef<
+ Record> | SchemaOf
+ >;
initialValues?: MaybeRef;
initialErrors?: Record;
initialTouched?: Record;
@@ -299,7 +302,7 @@ export function useForm = Record {
+ return formCtx.validateSchema('force').then(results => {
return keysOf(results)
.map(r => ({ key: r, errors: results[r].errors }))
.reduce(resultReducer, { errors: {}, valid: true });
@@ -381,6 +384,7 @@ export function useForm = Record = {
register: registerField,
unregister: unregisterField,
@@ -388,11 +392,11 @@ export function useForm = Record {
- return validateYupSchema(formCtx, shouldMutate);
+ validateSchema: isYupValidator(unref(schema))
+ ? mode => {
+ return validateYupSchema(formCtx, mode);
}
: undefined,
validate,
@@ -443,10 +447,16 @@ export function useForm = Record {
+ formCtx.validateSchema?.('validated-only');
+ });
+ }
+
// Provide injections
provide(FormContextSymbol, formCtx as FormContext);
provide(FormErrorsSymbol, errors);
@@ -508,9 +518,9 @@ function useFormMeta>(
async function validateYupSchema(
form: FormContext,
- shouldMutate?: boolean
+ mode: SchemaValidationMode
): Promise> {
- const errors: ValidationError[] = await (form.schema as YupValidator)
+ const errors: ValidationError[] = await (unref(form.schema) as YupValidator)
.validate(form.values, { abortEarly: false })
.then(() => [])
.catch((err: ValidationError) => {
@@ -541,14 +551,18 @@ async function validateYupSchema(
};
result[fieldId] = fieldResult;
+ if (mode === 'silent') {
+ applyFieldMutation(field, f => (f.meta.valid = fieldResult.valid));
- if (shouldMutate) {
- applyFieldMutation(field, f => f.setValidationState(fieldResult), true);
+ return result;
+ }
+ const wasValidated = Array.isArray(field) ? field.some(f => f.meta.validated) : field.meta.validated;
+ if (mode === 'validated-only' && !wasValidated) {
return result;
}
- applyFieldMutation(field, f => (f.meta.valid = fieldResult.valid));
+ applyFieldMutation(field, f => f.setValidationState(fieldResult), true);
return result;
}, {} as Record);
diff --git a/packages/vee-validate/tests/Form.spec.ts b/packages/vee-validate/tests/Form.spec.ts
index f13577971..73955aca6 100644
--- a/packages/vee-validate/tests/Form.spec.ts
+++ b/packages/vee-validate/tests/Form.spec.ts
@@ -2,7 +2,7 @@ import flushPromises from 'flush-promises';
import { defineRule } from '@/vee-validate';
import { mountWithHoc, setValue, setChecked, dispatchEvent } from './helpers';
import * as yup from 'yup';
-import { onErrorCaptured, reactive, ref, Ref } from 'vue';
+import { computed, onErrorCaptured, reactive, ref, Ref } from 'vue';
describe('', () => {
const REQUIRED_MESSAGE = `This field is required`;
@@ -1723,4 +1723,81 @@ describe('', () => {
expect(list?.children).toHaveLength(2);
expect(list?.textContent).toBe('badwrong');
});
+
+ test('supports computed yup schemas', async () => {
+ mountWithHoc({
+ setup() {
+ const acceptList = ref(['1', '2']);
+ const schema = computed(() => {
+ return yup.object({
+ password: yup.string().oneOf(acceptList.value),
+ });
+ });
+
+ return {
+ schema,
+ };
+ },
+ template: `
+
+
+ {{ errors.password }}
+
+ `,
+ });
+
+ await flushPromises();
+ const input = document.querySelector('input') as HTMLInputElement;
+ expect(document.querySelector('span')?.textContent).toBe('');
+ setValue(input, '3');
+ await flushPromises();
+ // 3 is not allowed yet
+ expect(document.querySelector('span')?.textContent).toBeTruthy();
+ await flushPromises();
+ // field is re-validated
+ setValue(input, '2');
+ await flushPromises();
+
+ expect(document.querySelector('span')?.textContent).toBe('');
+ });
+
+ test('re-validates when a computed yup schema changes', async () => {
+ const acceptList = ref(['1', '2']);
+ function addItem(item: string) {
+ acceptList.value.push(item);
+ }
+
+ mountWithHoc({
+ setup() {
+ const schema = computed(() => {
+ return yup.object({
+ password: yup.string().oneOf(acceptList.value),
+ });
+ });
+
+ return {
+ schema,
+ };
+ },
+ template: `
+
+
+ {{ errors.password }}
+
+ `,
+ });
+
+ await flushPromises();
+ const input = document.querySelector('input') as HTMLInputElement;
+ expect(document.querySelector('span')?.textContent).toBe('');
+ setValue(input, '3');
+ await flushPromises();
+ // 3 is not allowed yet
+ expect(document.querySelector('span')?.textContent).toBeTruthy();
+
+ // field is re-validated automatically
+ addItem('3');
+ await flushPromises();
+ expect(document.querySelector('span')?.textContent).toBe('');
+ });
});