Vee-Validate Plugin
The vee-validate plugin lets you validate your generated fields using vee-validate.
Installation
To install the plugin, simply add it to your package.json via terminal, you also need to add vee-validate.
yarn add vee-validate@next @formvuelate/plugin-vee-validate
# OR
npm i vee-validate@next @formvuelate/plugin-vee-validateUsage
To use the plugin, import and pass it to the SchemaFormFactory. This creates a SchemaForm component with validation capabilities.
import { SchemaFormFactory } from 'formvuelate'
import VeeValidatePlugin from '@formvuelate/plugin-vee-validate'
const SchemaFormWithValidation = SchemaFormFactory([
VeeValidatePlugin({
// plugin configuration here
})
])Now that the component is created, you can register it and use it in your template:
<template>
<div id="app">
<SchemaFormWithValidation :schema="mySchema" />
</div>
</template>
<script>
export default {
components: {
SchemaFormWithValidation
},
setup () {
[...]
}
}
</script>Component Requirements
Your components will receive the validation state via validation prop which contains the error messages and the meta information exposed by vee-validate, the validation prop contains the following properties/methods:
{
errorMessage, // The first error message for that field
errors, // All error messages for that field
meta, // A field meta object
setTouched, // Sets the meta `touched` flag
}You can opt-in to any of these properties or to the entire validation object. Here is an example FormText component that accepts the validation object as a prop:
<template>
<div>
<input :value="modelValue" @input="update($event.target.value)" />
<span>{{ validation.errorMessage }}</span>
</div>
</template>
<script>
export default {
props: {
// other props
modelValue: {
required: true
},
validation: {
type: Object,
default: () => ({})
}
},
methods: {
update (value) {
this.$emit('update:modelValue', value)
}
}
}
</script>Whenever the modelValue is updated the field will be validated immediately.
If we want the validations not to be immediate, or lazy, we can show the error message when the field is touched. A field is considered touched after the field loses focus.
In this case, we set it using the validation object's setTouched method as shown in the following example.
<template>
<div>
<input
:value="modelValue"
@input="update($event.target.value)"
@blur="onBlur"
/>
<span v-if="validation.meta.touched">{{ validation.errorMessage }}</span>
</div>
</template>
<script>
export default {
props: {
// ...
},
methods: {
update (value) {
this.$emit('update:modelValue', value)
},
onBlur () {
this.validation.setTouched(true)
}
}
}
</script>Note that when the form is submitted, all fields will be automatically "touched". Read here for information about form submission behavior in vee-validate.
Configuration
The VeeValidatePlugin accepts one parameter, a configuration object. Let's look at the properties that we can use.
mapProps
Important
This refers to Vee-validate's mapProps, and not to the mapProps property exposed by LookupPlugin.
If you are using 3rd party components and cannot modify their definition to accept the validation object, you can use the mapProps configuration to map the validation object to another property or multiple properties that are accepted by your component.
In the following example, the errorMessage is extracted to it's own prop.
const SchemaFormWithValidation = SchemaFormFactory([
VeeValidatePlugin({
mapProps (validation) {
return {
errorMessage: validation.errorMessage
}
}
})
])Now your component definition can accept the errorMessage prop instead of the entire validation object.
You can also map the validation prop based on your schema input type, so if you are using multiple 3rd party components you can provide each with the suitable validation props.
const SchemaFormWithValidation = SchemaFormFactory([
VeeValidatePlugin({
mapProps(validation, el) {
// If the field is the `FormText` component, send the entire validation object
if (el.component.name === 'FormText') {
return {
validation
}
}
// Otherwise send the error message only
return {
errorMessage: validation.errorMessage
}
}
})
])Defining Validation Rules
Now that your component is configured to receive validation state, let's take a look on how to actually validate them.
There are two approaches to specify validation rules to your schema fields, which are "field-level" and "form-level".
Field Level Validation
The "field-level" approach allows to you add a validations property to your fields schema, the validation property can be any type of validators that is accepted by vee-validate
Here is an example of a schema that uses all the possible validations value types:
import * as yup from 'yup'
const schema = {
email: {
component: FormText,
label: 'Email',
// Globally defined rules
validations: 'required|email'
},
password: {
component: FormText,
label: 'Password',
// Validation functions
validations: value => value && value.length > 6
},
fullName: {
component: FormText,
label: 'Full Name',
// yup validations
validations: yup.string().required()
}
}Then you can use the schema in your template
<div id="app">
<SchemaFormWithPlugin :schema="schema" />
</div>Form Level Validation
You can specify validations on the form level by passing a validation-schema prop to the component created by SchemaFormFactory, the validation-schema prop value should be one that accepted by vee-validate's form level validation.
This example uses yup to define validation schemas for your forms.
<template>
<div id="app">
<SchemaFormWithValidation
:schema="schema"
:validation-schema="validationSchema"
/>
</div>
</template>
<script>
import * as yup from 'yup'
export default {
components: {
SchemaFormWithValidation
},
setup() {
const schema = ref([
// Fields without the `validation` prop
])
const formData = ref({})
useSchemaForm(formData)
// The validation schema
const validationSchema = yup.object().shape({
email: yup
.string()
.email()
.required(),
password: yup
.string()
.min(5)
.required(),
fullName: yup.string().required()
})
return {
schema,
validationSchema
}
}
}
</script>Validation Messages Labels 2.4.0
Important
This section doesn't apply to validation schemas created with yup. You can use yup's own .label method to provide friendly display names for your fields.
By default the vee-validate plugin will use the field's model name in the generated messages for global string validators, this may produce unfriendly messages for your users.
In the following snippet, 'firstName' will be used as the field validation message.
const arraySchema = ref([
{
model: 'firstName',
// ...
}
])
const objectSchema = ref({
firstName: {
// ...
}
})For the required rule, it will produce this message:
The firstName field is requiredTo override this behavior and give your users better error messages you can include a label prop in each field's schema.
In the following snippet, 'First name' will be used as the field validation message.
const arraySchema = ref([
{
model: 'firstName',
label: 'First name',
}
])
const objectSchema = ref({
firstName: {
label: 'First name',
}
})For the required rule, it will produce this message:
The First name field is requiredHandling Submit
The VeeValidatePlugin automatically handles SchemaForm submits, and triggers validation before the form is submitted. You don't have to do anything special to trigger validations before submitting.
<template>
<SchemaForm @submit="onSubmit" :schema="schema">
<template #afterForm>
<button>Submit</button>
</template>
</SchemaForm>
</template>Note that the submit handler will be only executed if the form is valid.
Initial Form State
You can provide initial validation state to the SchemaForm, to set initial errors you can use the initial-errors prop:
Initial Errors
<template>
<SchemaForm :schema="schema" :initial-errors="initialErrors">
<template #afterForm>
<button>Submit</button>
</template>
</SchemaForm>
</template>
<script>
export default {
setup() {
const schema = ref([
// schema...
])
const formData = ref({})
useSchemaForm(formData)
const initialErrors = {
email: 'This email is already taken',
password: 'Password must be at least 8 characters long'
}
return {
schema,
initialErrors
}
}
}
</script>Initial Meta
You can provide initial-touched prop to set the initial touched meta flags for your schema fields:
<template>
<SchemaForm :schema="schema" :initial-touched="initialTouched">
<template #afterForm>
<button>Submit</button>
</template>
</SchemaForm>
</template>
<script>
export default {
setup() {
const schema = ref([
// schema...
])
const formData = ref({})
useSchemaForm(formData)
const initialTouched = {
email: true,
password: false
}
return {
formData,
schema,
initialErrors,
initialTouched
}
}
}
</script>SchemaForm slot props
Important
This feature is available starting on FormVuelate 3.2 and @formvuelate/plugin-vee-validate 2.3
You can access the form-level validation state by using either afterForm or beforeForm slot prop named validation on the SchemaForm component. Read more about available slots.
The form-level validation object contains the following properties:
isSubmitting: Indicates if the form is being submittedsubmitCount: Indicates the number of submission attemptsvalues: A record object containing fields/values pairserrors: A record object containing field/error pairsmeta: The form meta object.
<template>
<SchemaForm @submit="onSubmit" :schema="schema">
<template #afterForm="{ validation }">
<span>Form is submitting: {{ validation.isSubmitting }}</span>
<span>Attempted submits: {{ validation.submitCount }}</span>
<span>Values: {{ validation.values }}</span>
<span>Errors: {{ validation.errors }}</span>
<span>Metadata: {{ validation.meta }}</span>
</template>
</SchemaForm>
</template>The following are a few common examples for behaviors implemented with the form-level validation state.
Disable buttons until all fields are valid
<SchemaForm :schema="schema">
<template #afterForm="{ validation }">
<button :aria-disabled="!validation.meta.valid">Submit</button>
</template>
</SchemaForm>Learn more about accessible disabling of form buttons
Display spinner or loading state when the form is submitting
<SchemaForm :schema="schema">
<template #afterForm="{ validation }">
<button :class="{ 'is-submitting': validation.isSubmitting }">Submit</button>
</template>
</SchemaForm>Display all errors as a summary before the form fields
<SchemaForm :schema="schema">
<template #beforeForm="{ validation }">
<p>Please correct these errors</p>
<ul v-if="validation.errors">
<li v-for="error in validation.errors">{{ error }}</li>
</ul>
</template>
</SchemaForm>Accessing validation state outside the form 4.2.0
The slot props above are perfect when the UI that reacts to validation lives inside the SchemaForm. But sooner or later you will want that same state somewhere else on the page, think of a submit button pinned to a sticky footer, or a progress indicator in a sidebar that sits well outside the form element.
For those cases the plugin also emits the form-level validation state through an update:validation event, so you can bind it with v-model:validation and use it anywhere in your template:
<template>
<SchemaFormWithValidation
:schema="schema"
v-model:validation="validation"
/>
<!-- anywhere else, completely outside the form -->
<button :disabled="!validation.meta?.valid">
Save
</button>
</template>
<script setup>
import { ref } from 'vue'
const validation = ref({})
const schema = ref({ /* ... */ })
</script>The bound object carries the exact same properties as the slot prop: errors, values, isSubmitting, submitCount and meta. It is seeded with the initial state as soon as the form mounts, and refreshes on every change, so any reactive UI you point at it stays in sync.
Notice the ?. in the example. Your ref starts as an empty object and is only populated once the form mounts and emits, so reach for nested keys like meta.valid with optional chaining to stay safe on that very first render.
If you only care about reacting to changes and do not need to keep the value around, skip v-model and listen to the event directly:
<SchemaFormWithValidation
:schema="schema"
@update:validation="onValidationChange"
/>Multi-step (wizard) forms
FormVueLate ships a SchemaWizard component for stepped forms, but it renders the plain SchemaForm under the hood and does not run plugins, so it will not pick up vee-validate on its own. That is not a dead end. A wizard is really just a little glue: a <form> wrapper, a schema indexed by the current step, and a flag that keeps your model intact as the schema changes. Recreate that around your validated form and you get validation on every step.
The trick is to keep a single SchemaFormWithValidation mounted for the whole wizard and only swap its schema when the step changes. Because the component instance never unmounts, one vee-validate context lives across the entire flow. As the schema swaps, the previous step's fields unregister and the new ones register, so the form-level validation.meta.valid always reflects the step the user is looking at. That is exactly what you need to gate a Next button.
Pair it with preventModelCleanupOnSchemaChange so the data a user typed on step one survives when they move to step two and back.
<script setup>
import { ref, computed } from 'vue'
import { SchemaFormFactory, useSchemaForm } from 'formvuelate'
import VeeValidatePlugin from '@formvuelate/plugin-vee-validate'
import * as yup from 'yup'
const SchemaFormWithValidation = SchemaFormFactory([VeeValidatePlugin()])
const step = ref(0)
const userData = ref({})
useSchemaForm(userData)
const steps = [
// Step 1: name
{
firstName: { component: 'FormText', label: 'First name', validations: yup.string().required() },
lastName: { component: 'FormText', label: 'Last name', validations: yup.string().required() }
},
// Step 2: contact
{
email: { component: 'FormText', label: 'Email', validations: yup.string().email().required() }
}
]
const currentSchema = computed(() => steps[step.value])
const lastStep = steps.length - 1
const onSubmit = () => {
// Every step has been validated by now, userData holds the full result
console.log(userData.value)
}
</script>
<template>
<SchemaFormWithValidation
:schema="currentSchema"
preventModelCleanupOnSchemaChange
@submit="onSubmit"
>
<!-- Keep the nav in afterForm so the buttons render inside the <form> -->
<template #afterForm="{ validation }">
<button v-if="step > 0" type="button" @click="step--">Back</button>
<button
v-if="step < lastStep"
type="button"
:disabled="!validation.meta.valid"
@click="step++"
>
Next
</button>
<button v-else type="submit" :disabled="!validation.meta.valid">Submit</button>
</template>
</SchemaFormWithValidation>
</template>A few things worth pointing out:
validation.meta.validonly accounts for the fields currently on screen. That is the behavior you want for a stepper: the user advances one valid step at a time, and you never block Next on fields they have not reached yet.- Because the model is shared and cleanup is turned off, returning to an earlier step re-seeds every field with what the user already entered.
- If you would rather drive the buttons from somewhere outside the form, swap the slot prop for
v-model:validationand read the same state anywhere on the page. See Accessing validation state outside the form.
TIP
Keep your type="submit" button in the afterForm slot, just as you would with SchemaWizard, so it sits inside the generated <form> and triggers vee-validate's submission handling.