From 7b14c13210e367c9b8c7819ec59360eead62001b Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 29 Mar 2021 01:31:21 +0200 Subject: [PATCH 1/4] feat: added support for reactive schemas --- packages/vee-validate/src/Form.ts | 4 +- packages/vee-validate/src/types.ts | 2 +- packages/vee-validate/src/useField.ts | 2 +- packages/vee-validate/src/useForm.ts | 17 +++-- packages/vee-validate/tests/Form.spec.ts | 79 +++++++++++++++++++++++- 5 files changed, 96 insertions(+), 8 deletions(-) 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..d7c69bd89 100644 --- a/packages/vee-validate/src/types.ts +++ b/packages/vee-validate/src/types.ts @@ -113,7 +113,7 @@ export interface FormContext = Record>; submitCount: Ref; - schema?: Record> | SchemaOf; + schema?: MaybeRef> | SchemaOf>; validateSchema?: (shouldMutate?: boolean) => Promise>; validate(): Promise>; validateField(field: keyof TValues): Promise; diff --git a/packages/vee-validate/src/useField.ts b/packages/vee-validate/src/useField.ts index 0c0488362..b6a4b959c 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; } diff --git a/packages/vee-validate/src/useForm.ts b/packages/vee-validate/src/useForm.ts index bc28cb3cb..ed77187b0 100644 --- a/packages/vee-validate/src/useForm.ts +++ b/packages/vee-validate/src/useForm.ts @@ -29,7 +29,9 @@ import { import { FormErrorsSymbol, FormContextSymbol, FormInitialValuesSymbol } from './symbols'; interface FormOptions> { - validationSchema?: Record> | SchemaOf; + validationSchema?: MaybeRef< + Record> | SchemaOf + >; initialValues?: MaybeRef; initialErrors?: Record; initialTouched?: Record; @@ -381,6 +383,7 @@ export function useForm = Record = { register: registerField, unregister: unregisterField, @@ -388,9 +391,9 @@ export function useForm = Record { return validateYupSchema(formCtx, shouldMutate); } @@ -447,6 +450,12 @@ export function useForm = Record { + formCtx.validateSchema?.(true); + }); + } + // Provide injections provide(FormContextSymbol, formCtx as FormContext); provide(FormErrorsSymbol, errors); @@ -510,7 +519,7 @@ async function validateYupSchema( form: FormContext, shouldMutate?: boolean ): 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) => { 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(''); + }); }); From 9e8fd8ec0e3637fca3b5cc640f379440b7962142 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Thu, 1 Apr 2021 02:20:22 +0200 Subject: [PATCH 2/4] fix: only validate previously validated fields if the schema changes --- packages/vee-validate/src/types.ts | 9 ++++++++- packages/vee-validate/src/useField.ts | 7 +++++-- packages/vee-validate/src/useForm.ts | 23 ++++++++++++++--------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/vee-validate/src/types.ts b/packages/vee-validate/src/types.ts index d7c69bd89..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,6 +108,12 @@ 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; @@ -114,7 +121,7 @@ export interface FormContext = Record>; submitCount: Ref; schema?: MaybeRef> | SchemaOf>; - validateSchema?: (shouldMutate?: boolean) => Promise>; + 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 b6a4b959c..532ffe0ec 100644 --- a/packages/vee-validate/src/useField.ts +++ b/packages/vee-validate/src/useField.ts @@ -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 ed77187b0..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, @@ -301,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 }); @@ -394,8 +395,8 @@ export function useForm = Record { - return validateYupSchema(formCtx, shouldMutate); + ? mode => { + return validateYupSchema(formCtx, mode); } : undefined, validate, @@ -446,13 +447,13 @@ export function useForm = Record { - formCtx.validateSchema?.(true); + formCtx.validateSchema?.('validated-only'); }); } @@ -517,7 +518,7 @@ function useFormMeta>( async function validateYupSchema( form: FormContext, - shouldMutate?: boolean + mode: SchemaValidationMode ): Promise> { const errors: ValidationError[] = await (unref(form.schema) as YupValidator) .validate(form.values, { abortEarly: false }) @@ -550,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); From 5b5495a9e979d474cb1f5be2c3d7d975fe33d160 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sat, 3 Apr 2021 09:03:02 +0200 Subject: [PATCH 3/4] docs: added reactive schema docs --- docs/content/guide/components/validation.md | 23 +++++++++++++++++++ .../guide/composition-api/validation.md | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/docs/content/guide/components/validation.md b/docs/content/guide/components/validation.md index 18a60bda1..a5c98ca50 100644 --- a/docs/content/guide/components/validation.md +++ b/docs/content/guide/components/validation.md @@ -211,6 +211,29 @@ 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), + }); + }, + }, +}; +``` + +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..fd3e3d069 100644 --- a/docs/content/guide/composition-api/validation.md +++ b/docs/content/guide/composition-api/validation.md @@ -212,6 +212,29 @@ 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 { computed, ref } from 'vue'; +import * as yup from 'yup'; +import { useForm } from 'vee-validate'; + +const min = ref(0); +const schema = computed(() => { + return yup.object({ + password: yup.string().min(min.value) + }); +}); + +const { ... } = useForm({ + validationSchema: schema +}); +``` + +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 From 4485ccd1a7d8c456d25e7fb8433a0a3aad53b4f9 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Sat, 3 Apr 2021 09:33:25 +0200 Subject: [PATCH 4/4] docs: full examples for the reactive schema --- docs/content/guide/components/validation.md | 34 +++++++++++++++ .../guide/composition-api/validation.md | 41 ++++++++++++++----- 2 files changed, 64 insertions(+), 11 deletions(-) diff --git a/docs/content/guide/components/validation.md b/docs/content/guide/components/validation.md index a5c98ca50..8d9dc3ce8 100644 --- a/docs/content/guide/components/validation.md +++ b/docs/content/guide/components/validation.md @@ -232,6 +232,40 @@ export default { }; ``` +```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 diff --git a/docs/content/guide/composition-api/validation.md b/docs/content/guide/composition-api/validation.md index fd3e3d069..d9f239410 100644 --- a/docs/content/guide/composition-api/validation.md +++ b/docs/content/guide/composition-api/validation.md @@ -216,21 +216,40 @@ There is an official integration available for [Zod validation](https://github.c You can have reactive form schemas using `computed` if you are looking to create dynamic schemas using either `yup` or a validation object. -```js +```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.