Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added support for reactive schemas #3238

Merged
merged 4 commits into from Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -107,14 +108,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 @@ -16,6 +16,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