Handling Forms

vee-validate offers many helpers to handle form submissions, resets, and DX to make your forms much easier to reason about and less of a burden to maintain. The Form component handles the following form cases:

  • Submitting forms with JavaScript listeners (AJAX)
  • Submitting forms using the classic/native approach (page reload)
  • Handling form resets

Form Values

So far you probably noticed we didn’t use v-model once in the examples. This is because in most cases you don’t need the model values until you submit them to your API or not at all if you are submitting an HTML form without JavaScript.

Having to create models just to be able to reference them later is redundant and vee-validate goes around this by creating an internal model for the <Field /> field component instances and tracks them and keeps them in sync with the input. You can still use v-model if you need it but vee-validate doesn’t require it.

You may access your form’s values using the values scoped slot prop on the Form component:

vue<template>
  <Form v-slot="{ values }">
    <Field name="email" type="email" />
    <Field name="password" type="password" />

    <!-- print form values -->
    <pre>{{ values }}</pre>
  </Form>
</template>

<script setup>
import { Form, Field } from 'vee-validate';
</script>

You will rarely need to access the form values inside the template, but it is there if you ever need it. What’s interesting is that vee-validate follows the assumption that most likely you will need the form values at the submission phase.

So if you were to add a submit handler on the <Form /> component, vee-validate will automatically pass the form values to your handler as the first argument.

vue<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    <Field name="email" type="email" />
    <ErrorMessage name="email" />

    <Field name="password" type="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
});

function onSubmit(values) {
  // Submit values to API...
  alert(JSON.stringify(values, null, 2));
}
</script>

Handling Submissions

vee-validate exposes useful defaults to help you handle form submissions whether you submit them using JavaScript or native HTML submissions, in most cases, you would like to make sure all your fields are valid before you submit the form, this is done for you by default.

Consider the last example, if you tried submitting the form, it won’t be submitted unless all fields are valid. When rendering a form using the as prop, vee-validate will automatically listen for the submit event and prevent the execution of any submit listener you may have on the form.

If you have a submit listener on the Form component, vee-validate assumes you will be handling submissions via JavaScript (AJAX) and automatically calls event.preventDefault() for you and once the form is finished validating and turned out to be valid it will pass all the values of the <Field /> components.

But in the case when you don’t have a submit listener on your form, vee-validate assumes that the form will be submitted using the native HTML submission that causes the page to “reload”. However vee-validate will make sure the form is not submitted unless all fields are valid, here is an example:

vue<Form method="post" action="/api/users" :validation-schema="schema" />
  <Field name="email" />
  <Field name="name" type="email" />
  <Field name="password" type="password" />

  <button>Submit</button>
</Form>

Manually Handling Submissions

If you have complex markup requirements in your forms, you can use any of those <Form /> component slot props:

  • handleSubmit: automatically prevents native submission at all times, use for AJAX submissions
  • submitForm: prevents native submissions as long as the form is invalid, use for native submissions
  • validate: triggers validation on all fields belonging to the form

Using handleSubmit

The handleSubmit slot prop is probably the most common method you will use to handle form submissions manually, it accepts a callback that will be executed with the form values if the form is valid.

vue<template>
  <VeeForm v-slot="{ handleSubmit }" :validation-schema="schema" as="div">
    <form @submit="handleSubmit($event, onSubmit)">
      <Field name="email" type="email" />
      <ErrorMessage name="email" />

      <Field name="password" type="password" />
      <ErrorMessage name="password" />

      <button>Submit</button>
    </form>
  </VeeForm>
</template>

<script setup>
import { Form as VeeForm, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
});

function onSubmit(values) {
  // Submit values to API...
  alert(JSON.stringify(values, null, 2));
}
</script>

Using submitForm

Alternatively if you plan to submit forms natively which will cause a page “reload” then use submitForm as an event handler:

vue<template>
  <VeeForm v-slot="{ submitForm }" :validation-schema="schema" as="div">
    <form @submit="submitForm" method="post" action="/api/users/">
      <Field name="email" type="email" />
      <ErrorMessage name="email" />

      <Field name="password" type="password" />
      <ErrorMessage name="password" />

      <button>Submit</button>
    </form>
  </VeeForm>
</template>

<script setup>
import { Form as VeeForm, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
});
</script>

This will prevent submitting the form until all fields are valid.

Using validate()

You can validate the form without submissions using the validate() slot prop function:

vue<Form v-slot="{ validate }" :validation-schema="schema">
  <Field name="email" type="email" />
  <ErrorMessage name="email" />

  <Field name="password" type="password" />
  <ErrorMessage name="password" />

  <button type="button" @click="validate">Submit</button>
</Form>

That form doesn’t render a form tag, so vee-validate doesn’t handle submissions for that group of fields. But you can still validate them using the validate function present on the Form component slot props.

Submission Progress

Quite often you need to show your users a submission indicator, or you might want to disable the submit button entirely until the submission attempt is done. The Form component offers an isSubmitting slot prop that you can use to show such UI indicators.

The isSubmitting state will be set to true once the validation of the form starts (as a result of a submit event) and will keep track of the submission handler you passed to either onSubmit or until it calls submitForm. If the submission handler throws any errors or completes successfully it will be set to false afterward.

Here is small example:

See the Pen vee-validate v4 isSubmitting by Abdelrahman Awad ( @logaretm) on CodePen.

isSubmitting and validate()

Note that calling validate from the Form slot props will not cause the isSubmitting state to change, it will only change if either submitForm or handleSubmit are called or when a submit event is triggered.

submitCount

The Form component exposes a submitCount state that you can use to track the number of submission attempts done by the user. For more information check the API Reference.

Handling Invalid Submissions

In case you want to perform some logic after a form fails to submit due to validation errors (e.g: focusing the first invalid field), you can listen for the onInvalidSubmit event emitted by the <Form /> component.

vue<template>
  <Form @submit="onSubmit" :validation-schema="schema" @invalid-submit="onInvalidSubmit">
    <Field name="email" type="email" />
    <ErrorMessage name="email" />

    <Field name="password" type="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
});

function onSubmit(values) {
  // Submit values to API...
  alert(JSON.stringify(values, null, 2));
}

function onInvalidSubmit({ values, errors, results }) {
  console.log(values); // current form values
  console.log(errors); // a map of field names and their first error message
  console.log(results); // a detailed map of field names and their validation results
}
</script>

Specifying a onInvalidSubmit prop or @invalid-submit will run your handler if you submit your form using either handleSubmit or the regular form submit event but not the submitForm function.

Initial Values

Since with vee-validate you don’t have to use v-model to track your values, the Form component allows you to define the starting values for your fields, by default all fields start with undefined as a value.

Using the initialValues prop you can send an object that contains the field names as keys and their values:

vue<template>
  <Form :validation-schema="schema" :initial-values="formValues">
    <Field name="email" type="email" />
    <ErrorMessage name="email" />

    <Field name="password" type="password" />
    <ErrorMessage name="password" />

    <button type="Submit">Submit</button>
  </Form>
</template>

<script setup>
import { Form, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
});

// Initial values
const formValues = {
  email: 'example@example.com',
  password: 'P@$$w0Rd',
};
</script>

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.

Note that only the non-dirty fields will be updated. In other words, only the fields that were not manipulated by the user will be updated. For information on how to set the values for all fields regardless of their dirty status check the following Setting Form Values section

tip

It’s generally recommended that you provide the initialValues, this is because vee-validate cannot assume a reasonable initial value for your fields other than undefined which may cause unexpected behavior when using a 3rd-party validator that does not deal with undefined.

Setting Form Values

You can set any field’s value using either setFieldValue or setValues, both methods are exposed on the <Form /> component scoped slot props, and as component instance methods.

You can call them with template $refs and for an added convenience you can call them in the submit handler callback.

Using scoped slot props

vue<Form v-slot="{ setFieldValue, setValues }">
  <Field name="email"  />
  <ErrorMessage name="email" />

  <Field name="password"  />
  <ErrorMessage name="password" />

  <button type="button" @click="setFieldValue('email', 'test')">Set Field Value</button>
  <button type="button" @click="setValues({ email: 'test', password: 'test12' })">
    Set Multiple Values
  </button>
</Form>

Using submit callback

vue<template>
  <Form @submit="onSubmit">
    <Field name="email" />
    <ErrorMessage name="email" />

    <Field name="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
function onSubmit(values, actions) {
  // Submit the values...

  // set single field value
  actions.setFieldValue('email', 'ummm@example.com');

  // set multiple values
  actions.setValues({
    email: 'ummm@example.com',
    password: 'P@$$w0Rd',
  });
}
</script>

Using template $refs

vue<template>
  <Form @submit="onSubmit" ref="myForm">
    <Field name="email" />
    <ErrorMessage name="email" />

    <Field name="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
function onSubmit(values) {
  // Submit the values...

  // set single field value
  this.$refs.myForm.setFieldValue('email', 'ummm@example.com');

  // set multiple values
  this.$refs.myForm.setValues({
    email: 'ummm@example.com',
    password: 'P@$$w0Rd',
  });
}
</script>

Note that setting any field’s value using this way will trigger validation

Submission Behavior

vee-validate does the following when you submit a form rendered by <Form /> or when calling either handleSubmit or submitForm:

Before validation stage

  • Sets all fields touched meta to true
  • Sets isSubmitting form state to true
  • Increments the submitCount form state by 1

Validation stage

  • Sets form and individual fields meta pending to true to indicate validation is in progress
  • Runs the validation function/schema/rule against the current form values asynchronously
  • Checks for any errors in the validation result
    • If there are errors then it will skip the next stage and update the validation state (meta, errors) for the form and fields
    • If there aren’t any errors then it will set the pending meta flag to false and proceed to the next stage

After validation stage

  • Calls the @submit handler you specified, or calls the handleSubmit callback you provided.
  • After the callbacks in either method finish (it will wait if the result is asynchronous), then it will set isSubmitting to false

Note that there isn’t a need to have isSubmitting set back to false if you’ve used submitForm, as this submission method will perform a full-page refresh (native forms behavior).

Handling Resets

vee-validate also handles form resets in a similar way to submissions. When resetting the form, all fields’ errors and meta flags will be reset to their original state, including the fields’ values.

Form reset is handled automatically if you are using the as prop to render a form element, like shown in this example:

vue<template>
  <Form :validation-schema="schema">
    <Field name="email" type="email" />
    <ErrorMessage name="email" />

    <Field name="password" type="password" />
    <ErrorMessage name="password" />

    <button type="Submit">Submit</button>
    <button type="reset">Reset</button>
  </Form>
</template>

<script setup>
import { Form, Field } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),
  password: yup.string().required().min(8),
});
</script>

Alternatively if you plan to use the scoped slot for complex markup, you can use the handleReset slot prop function to trigger the reset manually:

vue<Form v-slot="{ handleReset }" :validation-schema="schema">
  <Field name="email" type="email" />
  <ErrorMessage name="email" />

  <Field name="password" type="password" />
  <ErrorMessage name="password" />

  <button type="button" @click="handleReset">Reset</button>
</Form>

Resetting Forms After Submit

Usually you will reset your forms after a successful submission, the onSubmit handler receives an additional FormActions object in the second argument that allows you do some actions on the form after submissions, this is the shape of the FormActions object:

tsexport interface FormActions {
  setFieldValue: (field: T, value: any) => void;
  setFieldError: (field: string, message: string | undefined) => void;
  setErrors: (fields: Partial<Record<string, string | undefined>>) => void;
  setValues: (fields: Partial<Record<T, any>>) => void;
  setFieldTouched: (field: string, isTouched: boolean) => void;
  setTouched: (fields: Partial<Record<string, boolean>>) => void;
  resetForm: (state?: Partial<FormState>) => void;
}

This is an example of using the form actions object to reset the form:

vue<template>
  <Form @submit="onSubmit" :validation-schema="schema">
    <!-- fields ... -->
  </Form>
</template>

<script setup>
import { Form, Field } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  // ...
});

function onSubmit(values, { resetForm }) {
  console.log(values); // send data to API
  // reset the form and the field values to their initial values
  resetForm();
}
</script>

The resetForm accepts an optional state object that allows you to specify the new initial values for any of the fields state, this is the shape of the FormState object:

tsinterface FormState {
  // any error messages
  errors: Record<string, string>;
  // dirty meta flags
  dirty: Record<string, boolean>;
  // touched meta flags
  touched: Record<string, boolean>;
  // Form Values
  values: Record<string, any>;
}

In the following snippet, resetForm is used to update the form values to specific ones other than their original values. This is useful if your receive your form state asynchronously

jsresetForm({
  values: {
    email: 'example@example.com',
    password: '',
  },
});

You can also use template $refs to reset the form whenever you need:

vue<template>
  <Form ref="form" @submit="onSubmit" :validation-schema="schema">
    <!-- fields ... -->

    <button type="Submit">Submit</button>
  </Form>
</template>

<script setup>
import { Form, Field } from 'vee-validate';
import * as yup from 'yup';

const schema = yup.object({
  // ...
});

function onSubmit(values) {
  console.log(values); // send data to API

  // reset the form
  this.$refs.form.resetForm();
}
</script>

Initial Errors

If you are building a non-SPA application it is very common to pre-fill form errors using server-side rendering, frameworks like Laravel make this very easy to do. vee-validate supports filling the errors initially before any validation is done using the initialErrors prop which is present on the <Form /> component scoped slot props.

The initialErrors property accepts an object containing the field names as keys with their corresponding error message string.

vue<template>
  <Form :initial-errors="initialErrors">
    <Field name="email" />
    <ErrorMessage name="email" />

    <Field name="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
const initialErrors = {
  email: 'This email is already taken',
  password: 'The password is too short',
};
</script>

tip

initialErrors are applied once the Form component is mounted and is ignored after, so any changes to the initialErrors props won’t affect the messages.

See the next section for setting errors manually.

Setting Errors Manually

Quite often you will find yourself unable to replicate some validation rules on the client-side due to natural limitations. For example, unique email validation is complex to implement on the client-side, which is why the <Form /> component allows you to set errors manually.

You can set messages for fields by using either setFieldError which sets an error message for one field at a time, and the setErrors function which allows you to set error messages for multiple fields at once.

Both functions are available on the Form component scoped slot props, and also on the Form component instance which enables you to use it with template $refs, and also for added convenience on the submit event handler since it would be the most common place for its usage.

Here are a few snippets showcasing its usage in these various scenarios:

Using scoped slot props (recommended)

vue<Form v-slot="{ setFieldError, setErrors }">
  <Field name="email"  />
  <ErrorMessage name="email" />

  <Field name="password"  />
  <ErrorMessage name="password" />

  <button type="button" @click="setFieldError('email', 'nope')">Set Single Error</button>
  <button type="button" @click="setErrors({ email: 'nope', password: 'wrong' })">
    Set Multiple Errors
  </button>
</Form>

Using submit callback (recommended)

vue<template>
  <Form @submit="onSubmit">
    <Field name="email" />
    <ErrorMessage name="email" />

    <Field name="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
function onSubmit(values, actions) {
  // Submit the values...

  // set single field error
  actions.setFieldError('email', 'this email is already taken');

  // set multiple errors
  actions.setErrors({
    email: 'this field is already taken',
    password: 'someone already has this password',
  });
}
</script>

Using template $refs

vue<template>
  <Form @submit="onSubmit" ref="myForm">
    <Field name="email" />
    <ErrorMessage name="email" />

    <Field name="password" />
    <ErrorMessage name="password" />

    <button>Submit</button>
  </Form>
</template>

<script setup>
function onSubmit(values) {
  // Submit the values...

  // if API returns errors
  this.$refs.myForm.setFieldError('email', 'this email is already taken');
  this.$refs.myForm.setErrors({
    email: 'this field is already taken',
    password: 'someone already has this password',
  });
}
</script>

Avoid Template $refs

Always try to avoid using the template $refs to gain access to the <Form /> component methods, template $refs are designed to be an escape hatch of sorts when all else fails.

So treat them as such and don’t reach out for template $refs if you can help it.

Next Step