Skip to content

Commit

Permalink
feat: added validateOnMount prop to Field and Form components (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
logaretm committed Oct 6, 2020
1 parent b8dafbd commit 3a0d878
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 35 deletions.
18 changes: 9 additions & 9 deletions docs/content/api/field.md
Expand Up @@ -74,15 +74,15 @@ When using `v-slot` on the `Field` component you no longer have to provide an `a

### Props

| Prop | Type | Required/Default | Description |
| :-------- | :----------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------- |
| as | `string` | `"span"` | The element to render as a root node, defaults to `input` |
| name | `string` | Required | The field's name, must be inside `<Form />` |
| rules | `object \| string \| Function` | `null` | The field's validation rules |
| immediate | `boolean` | `false` | If true, field will be validated on mounted |
| bails | `boolean` | `true` | Stops validating as soon as a rule fails the validation |
| disabled | `disabled` | `false` | Disables validation and the field will no longer participate in the parent form state |
| label | `string` | `undefined` | A different string to override the field `name` prop in error messages, useful for display better or formatted names |
| Prop | Type | Required/Default | Description |
| :-------------- | :----------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------- |
| as | `string` | `"span"` | The element to render as a root node, defaults to `input` |
| name | `string` | Required | The field's name, must be inside `<Form />` |
| rules | `object \| string \| Function` | `null` | The field's validation rules |
| validateOnMount | `boolean` | `false` | If true, field will be validated when the component is mounted |
| bails | `boolean` | `true` | Stops validating as soon as a rule fails the validation |
| disabled | `disabled` | `false` | Disables validation and the field will no longer participate in the parent form state |
| label | `string` | `undefined` | A different string to override the field `name` prop in error messages, useful for display better or formatted names |

### Slots

Expand Down
11 changes: 6 additions & 5 deletions docs/content/api/form.md
Expand Up @@ -99,11 +99,12 @@ While not recommended, you can make the `Form` component a renderless component

### Props

| Prop | Type | Required/Default | Description |
| :--------------- | :--------------------- | :--------------- | :-------------------------------------------------------------------------------------------- |
| as | `string` | `"form"` | The element to render as a root node |
| validationSchema | `Record<string, string | Function>` | `undefined` | The element to render as a root node |
| initialValues | `Record<string, any>` | `undefined` | Initial values to fill the fields with, when provided the fields will be validated on mounted |
| Prop | Type | Default | Description |
| :--------------- | :----------------------------------- | :---------- | :----------------------------------------------------------------------------------------------------------- |
| as | `string` | `"form"` | The element to render as a root node |
| validationSchema | `Record<string, string \| Function>` | `undefined` | An object describing a schema to validate fields with, can be a plain object or a `yup` object schema |
| initialValues | `Record<string, any>` | `undefined` | Initial values to fill the fields with, when provided the fields will be validated on mounted |
| validateOnMount | `boolean` | `false` | If true, the fields currently present in the form will be validated when the `<Form />` component is mounted |

### Slots

Expand Down
2 changes: 1 addition & 1 deletion docs/content/api/use-field.md
Expand Up @@ -98,7 +98,7 @@ The full signature of the `useField` function looks like this:
interface FieldOptions {
initialValue: any; // the initial value, cannot be a ref
disabled: MaybeReactive<boolean>; // if the input is disabled, can be a ref
immediate?: boolean; // if the field should be validated on mounted
validateOnMount?: boolean; // if the field should be validated when the component is mounted
bails?: boolean; // if the field validation should run all validations
form?: FormController; // the Form object returned from `useForm` to associate this field with
label?: string; // A friendly name to be used in `generateMessage` config instead of the field name
Expand Down
2 changes: 2 additions & 0 deletions docs/content/guide/handling-forms.md
Expand Up @@ -368,4 +368,6 @@ export default {

Doing so will trigger initial validation on the form and it will generate messages for fields that fail the initial validation. You can still use `v-model` on your fields to define model-based initial values.

You can use `validateOnMount` prop present on the `<Form />` component to force an initial validation when the component is mounted.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
13 changes: 13 additions & 0 deletions docs/content/guide/validation.md
Expand Up @@ -260,6 +260,19 @@ This is slightly verbose, but this gives you exact control on which events trigg

`useField()` composition function is not concerned with any events, it only validates whenever the `value` ref changes. It gives you everything you need to setup your own validation experience.

In addition to those events, you can also validate when the `<Field />` or `<Form />` components are mounted with `validateOnMount` prop present on both components:

```vue
<!-- Trigger validation when this field is mounted (initial validation) -->
</Field name="name" validate-on-mount />
<!-- Trigger validation on all fields inside this form when the form is mounted -->
<Form validate-on-mount >
</Field name="email" />
</Field name="password" />
</Form>
```

## Displaying Error Messages

### Using the Field slot-props
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/Field.ts
Expand Up @@ -19,7 +19,7 @@ export const Field = defineComponent({
type: [Object, String, Function],
default: null,
},
immediate: {
validateOnMount: {
type: Boolean,
default: false,
},
Expand Down Expand Up @@ -52,7 +52,7 @@ export const Field = defineComponent({
aria,
checked,
} = useField(props.name, rules, {
immediate: props.immediate,
validateOnMount: props.validateOnMount,
bails: props.bails,
disabled,
type: ctx.attrs.type as string,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/Form.ts
Expand Up @@ -19,11 +19,16 @@ export const Form = defineComponent({
type: Object,
default: undefined,
},
validateOnMount: {
type: Boolean,
default: false,
},
},
setup(props, ctx) {
const { errors, validate, handleSubmit, handleReset, values, meta, isSubmitting, submitForm } = useForm({
validationSchema: props.validationSchema,
initialValues: props.initialValues,
validateOnMount: props.validateOnMount,
});

const onSubmit = ctx.attrs.onSubmit ? handleSubmit(ctx.attrs.onSubmit as SubmissionHandler) : submitForm;
Expand Down
8 changes: 4 additions & 4 deletions packages/core/src/useField.ts
Expand Up @@ -23,7 +23,7 @@ import { FormInitialValues, FormSymbol } from './symbols';
interface FieldOptions {
initialValue: any;
disabled: MaybeReactive<boolean>;
immediate?: boolean;
validateOnMount?: boolean;
bails?: boolean;
form?: FormController;
type?: string;
Expand All @@ -37,7 +37,7 @@ type RuleExpression = MaybeReactive<string | Record<string, any> | GenericValida
* Creates a field composite.
*/
export function useField(name: string, rules: RuleExpression, opts?: Partial<FieldOptions>) {
const { initialValue, form, immediate, bails, disabled, type, valueProp, label } = normalizeOptions(name, opts);
const { initialValue, form, validateOnMount, bails, disabled, type, valueProp, label } = normalizeOptions(name, opts);

const { meta, errors, handleBlur, handleChange, handleInput, reset, patch, value, checked } = useValidationState({
name,
Expand Down Expand Up @@ -81,7 +81,7 @@ export function useField(name: string, rules: RuleExpression, opts?: Partial<Fie

onMounted(() => {
runValidation().then(result => {
if (immediate) {
if (validateOnMount) {
patch(result);
}
});
Expand Down Expand Up @@ -179,7 +179,7 @@ function normalizeOptions(name: string, opts: Partial<FieldOptions> | undefined)

const defaults = () => ({
initialValue: undefined,
immediate: false,
validateOnMount: false,
bails: true,
rules: '',
disabled: false,
Expand Down
9 changes: 8 additions & 1 deletion packages/core/src/useForm.ts
@@ -1,4 +1,4 @@
import { computed, ref, Ref, provide, reactive } from 'vue';
import { computed, ref, Ref, provide, reactive, onMounted } from 'vue';
import type { ValidationError } from 'yup';
import type { useField } from './useField';
import {
Expand All @@ -16,6 +16,7 @@ import { FormErrorsSymbol, FormInitialValues, FormSymbol } from './symbols';
interface FormOptions {
validationSchema?: Record<string, GenericValidateFunction | string | Record<string, any>>;
initialValues?: Record<string, any>;
validateOnMount?: boolean;
}

type FieldComposite = ReturnType<typeof useField>;
Expand Down Expand Up @@ -220,6 +221,12 @@ export function useForm(opts?: FormOptions) {
provide(FormErrorsSymbol, errors);
provide(FormInitialValues, opts?.initialValues || {});

onMounted(() => {
if (opts?.validateOnMount) {
validate();
}
});

return {
errors,
meta,
Expand Down
4 changes: 2 additions & 2 deletions packages/core/tests/Field.spec.ts
Expand Up @@ -194,11 +194,11 @@ describe('<Field />', () => {
expect(error.textContent).toBe('');
});

test('validates initially with immediate prop', async () => {
test('validates initially with validateOnMount prop', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="field" immediate rules="required" v-slot="{ field, errors }">
<Field name="field" validateOnMount rules="required" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down
36 changes: 36 additions & 0 deletions packages/core/tests/Form.spec.ts
Expand Up @@ -1032,4 +1032,40 @@ describe('<Form />', () => {
await flushPromises();
expect(fn).toHaveBeenCalledWith({ 'user.name': '12', 'user.addresses.0': 'abc' });
});

test('validate fields on mount with validateOnMount = true', async () => {
const wrapper = mountWithHoc({
setup() {
const schema = yup.object().shape({
email: yup.string().required().email(),
password: yup.string().required().min(8),
});

return {
schema,
};
},
template: `
<VForm @submit="submit" as="form" :validationSchema="schema" validateOnMount v-slot="{ errors }">
<Field id="email" name="email" as="input" />
<span id="emailErr">{{ errors.email }}</span>
<Field id="password" name="password" as="input" type="password" />
<span id="passwordErr">{{ errors.password }}</span>
<button>Validate</button>
</VForm>
`,
});

await flushPromises();

const emailError = wrapper.$el.querySelector('#emailErr');
const passwordError = wrapper.$el.querySelector('#passwordErr');

await flushPromises();

expect(emailError.textContent).toBe('email is a required field');
expect(passwordError.textContent).toBe('password is a required field');
});
});
22 changes: 11 additions & 11 deletions packages/i18n/tests/index.spec.ts
Expand Up @@ -25,7 +25,7 @@ test('can define new locales', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="field" immediate rules="required" v-slot="{ field, errors }">
<Field name="field" validateOnMount rules="required" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -55,12 +55,12 @@ test('can define specific messages for specific fields', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="test" :immediate="true" rules="required" v-slot="{ field, errors }">
<Field name="test" :validateOnMount="true" rules="required" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span class="error">{{ errors[0] }}</span>
</Field>
<Field name="name" :immediate="true" rules="required" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" rules="required" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span class="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -90,7 +90,7 @@ test('can merge locales without setting the current one', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="field" :immediate="true" rules="required" v-slot="{ field, errors }">
<Field name="field" :validateOnMount="true" rules="required" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -149,7 +149,7 @@ test('can switch between locales with setLocale', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="field" immediate rules="required" v-slot="{ field, errors }">
<Field name="field" validateOnMount rules="required" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -182,7 +182,7 @@ test('interpolates object params with short format', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="name" :immediate="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -210,7 +210,7 @@ test('interpolates object params with extended format', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="name" :immediate="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" :rules="{ between: { min: 1, max: 10 } }" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -238,7 +238,7 @@ test('interpolates array params', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="name" :immediate="true" :rules="{ between: [1, 10] }" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" :rules="{ between: [1, 10] }" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -266,7 +266,7 @@ test('interpolates string params', async () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="name" :immediate="true" rules="between:1,10" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" rules="between:1,10" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -295,7 +295,7 @@ describe('interpolation preserves placeholders if not found', () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="name" :immediate="true" :rules="{ between: [1] }" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" :rules="{ between: [1] }" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down Expand Up @@ -323,7 +323,7 @@ describe('interpolation preserves placeholders if not found', () => {
const wrapper = mountWithHoc({
template: `
<div>
<Field name="name" :immediate="true" :rules="{ between: { min: 0 } }" v-slot="{ field, errors }">
<Field name="name" :validateOnMount="true" :rules="{ between: { min: 0 } }" v-slot="{ field, errors }">
<input v-bind="field" type="text">
<span id="error">{{ errors[0] }}</span>
</Field>
Expand Down

0 comments on commit 3a0d878

Please sign in to comment.