Skip to content

Commit

Permalink
feat: added support for reactive schemas (#3238)
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Apr 7, 2021
1 parent 1fad5bb commit 295d656
Show file tree
Hide file tree
Showing 7 changed files with 221 additions and 19 deletions.
57 changes: 57 additions & 0 deletions docs/content/guide/components/validation.md
Expand Up @@ -211,6 +211,63 @@ There is an official integration available for [Zod validation](https://github.c

</doc-tip>

### 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
<template>
<Form @submit="submit" :validation-schema="schema">
<Field name="password" type="password" />
<ErrorMessage name="password" />
<button>Submit</button>
</Form>
</template>
<script>
import { Form, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';
export default {
components: {
Form,
Field,
ErrorMessage,
},
data: () => ({
min: 6,
}),
computed: {
schema() {
return yup.object({
password: yup.string().min(this.min),
});
},
},
};
</script>
```

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:
Expand Down
42 changes: 42 additions & 0 deletions docs/content/guide/composition-api/validation.md
Expand Up @@ -212,6 +212,48 @@ There is an official integration available for [Zod validation](https://github.c

</doc-tip>

### 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
<template>
<input name="password" v-model="password" type="password" />
<span>{{ passwordError }}</span>
</template>
<script>
import { computed, ref } from 'vue';
import { useForm, useField } from 'vee-validate';
import * as yup from 'yup';
export default {
setup() {
const min = ref(0);
const schema = computed(() => {
return yup.object({
password: yup.string().min(min.value),
});
});
// Create a form context with the validation schema
useForm({
validationSchema: schema,
});
const { value: password, errorMessage: passwordError } = useField('password');
return {
password,
passwordError,
};
},
};
</script>
```

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

<doc-tip>
Expand Down
4 changes: 3 additions & 1 deletion packages/vee-validate/src/Form.ts
Expand Up @@ -38,6 +38,8 @@ export const Form = defineComponent({
},
setup(props, ctx) {
const initialValues = toRef(props, 'initialValues');
const validationSchema = toRef(props, 'validationSchema');

const {
errors,
values,
Expand All @@ -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,
Expand Down
11 changes: 9 additions & 2 deletions packages/vee-validate/src/types.ts
Expand Up @@ -25,6 +25,7 @@ export interface FieldMeta<TValue> {
touched: boolean;
dirty: boolean;
valid: boolean;
validated: boolean;
pending: boolean;
initialValue?: TValue;
}
Expand Down Expand Up @@ -105,14 +106,20 @@ export type SubmissionHandler<TValues extends Record<string, unknown> = Record<s
ctx: SubmissionContext<TValues>
) => 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<TValues extends Record<string, any> = Record<string, any>> extends FormActions<TValues> {
register(field: PrivateFieldComposite): void;
unregister(field: PrivateFieldComposite): void;
values: TValues;
fieldsById: ComputedRef<Record<keyof TValues, PrivateFieldComposite | PrivateFieldComposite[]>>;
submitCount: Ref<number>;
schema?: Record<keyof TValues, GenericValidateFunction | string | Record<string, any>> | SchemaOf<TValues>;
validateSchema?: (shouldMutate?: boolean) => Promise<Record<keyof TValues, ValidationResult>>;
schema?: MaybeRef<Record<keyof TValues, GenericValidateFunction | string | Record<string, any>> | SchemaOf<TValues>>;
validateSchema?: (mode: SchemaValidationMode) => Promise<Record<keyof TValues, ValidationResult>>;
validate(): Promise<FormValidationResult<TValues>>;
validateField(field: keyof TValues): Promise<ValidationResult>;
errorBag: Ref<FormErrorBag<TValues>>;
Expand Down
9 changes: 6 additions & 3 deletions packages/vee-validate/src/useField.ts
Expand Up @@ -102,7 +102,7 @@ export function useField<TValue = unknown>(

const normalizedRules = computed(() => {
let rulesValue = unref(rules);
const schema = form?.schema;
const schema = unref(form?.schema);
if (schema && !isYupValidator(schema)) {
rulesValue = extractRuleFromSchema<TValue>(schema, unref(name)) || rulesValue;
}
Expand All @@ -117,14 +117,15 @@ export function useField<TValue = unknown>(
async function validateWithStateMutation(): Promise<ValidationResult> {
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),
values: form?.values ?? {},
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;
Expand All @@ -141,7 +142,7 @@ export function useField<TValue = unknown>(
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;
Expand Down Expand Up @@ -392,6 +393,7 @@ function useValidationState<TValue>({
setErrors(state?.errors || []);
meta.touched = state?.touched ?? false;
meta.pending = false;
meta.validated = false;
}

return {
Expand All @@ -416,6 +418,7 @@ function useMeta<TValue>(initialValue: MaybeRef<TValue>, currentValue: Ref<TValu
touched: false,
pending: false,
valid: true,
validated: false,
initialValue: computed(() => unref(initialValue) as TValue | undefined),
dirty: computed(() => {
return !isEqual(currentValue.value, unref(initialValue));
Expand Down
38 changes: 26 additions & 12 deletions packages/vee-validate/src/useForm.ts
Expand Up @@ -15,6 +15,7 @@ import {
PublicFormContext,
FormErrors,
FormErrorBag,
SchemaValidationMode,
} from './types';
import {
applyFieldMutation,
Expand All @@ -29,7 +30,9 @@ import {
import { FormErrorsSymbol, FormContextSymbol, FormInitialValuesSymbol } from './symbols';

interface FormOptions<TValues extends Record<string, any>> {
validationSchema?: Record<keyof TValues, GenericValidateFunction | string | Record<string, any>> | SchemaOf<TValues>;
validationSchema?: MaybeRef<
Record<keyof TValues, GenericValidateFunction | string | Record<string, any>> | SchemaOf<TValues>
>;
initialValues?: MaybeRef<TValues>;
initialErrors?: Record<keyof TValues, string | undefined>;
initialTouched?: Record<keyof TValues, boolean>;
Expand Down Expand Up @@ -299,7 +302,7 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
}

if (formCtx.validateSchema) {
return formCtx.validateSchema(true).then(results => {
return formCtx.validateSchema('force').then(results => {
return keysOf(results)
.map(r => ({ key: r, errors: results[r].errors }))
.reduce(resultReducer, { errors: {}, valid: true });
Expand Down Expand Up @@ -381,18 +384,19 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
setInPath(initialValues.value, path, value);
}

const schema = opts?.validationSchema;
const formCtx: FormContext<TValues> = {
register: registerField,
unregister: unregisterField,
fieldsById,
values: formValues,
setFieldErrorBag,
errorBag,
schema: opts?.validationSchema,
schema,
submitCount,
validateSchema: isYupValidator(opts?.validationSchema)
? shouldMutate => {
return validateYupSchema(formCtx, shouldMutate);
validateSchema: isYupValidator(unref(schema))
? mode => {
return validateYupSchema(formCtx, mode);
}
: undefined,
validate,
Expand Down Expand Up @@ -443,10 +447,16 @@ export function useForm<TValues extends Record<string, any> = Record<string, any
// otherwise run initial silent validation through schema if available
// the useField should skip their own silent validation if a yup schema is present
if (formCtx.validateSchema) {
formCtx.validateSchema(false);
formCtx.validateSchema('silent');
}
});

if (isRef(schema)) {
watch(schema, () => {
formCtx.validateSchema?.('validated-only');
});
}

// Provide injections
provide(FormContextSymbol, formCtx as FormContext);
provide(FormErrorsSymbol, errors);
Expand Down Expand Up @@ -508,9 +518,9 @@ function useFormMeta<TValues extends Record<string, unknown>>(

async function validateYupSchema<TValues>(
form: FormContext<TValues>,
shouldMutate?: boolean
mode: SchemaValidationMode
): Promise<Record<keyof TValues, ValidationResult>> {
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) => {
Expand Down Expand Up @@ -541,14 +551,18 @@ async function validateYupSchema<TValues>(
};

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<keyof TValues, ValidationResult>);
Expand Down

0 comments on commit 295d656

Please sign in to comment.