Forms

Form

Collect and validate form data.

Usage

Use the Form component to validate form data using schema libraries such as Yup, Zod, Joi, Valibot or your own validation logic. It works seamlessly with the FormGroup component to automatically display error messages around form elements.

The Form component requires the validate and state props for form validation.

  • state - a reactive object that holds the current state of the form.
  • validate - a function that takes the form's state as input and returns an array of FormError objects with the following fields:
    • message - the error message to display.
    • path - the path to the form element matching the name.
<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '#ui/types'

const state = reactive({
  email: undefined,
  password: undefined
})

const validate = (state: any): FormError[] => {
  const errors = []
  if (!state.email) errors.push({ path: 'email', message: 'Required' })
  if (!state.password) errors.push({ path: 'password', message: 'Required' })
  return errors
}

async function onSubmit (event: FormSubmitEvent<any>) {
  // Do something with data
  console.log(event.data)
}
</script>

<template>
  <UForm :validate="validate" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Schema

You can provide a schema from Yup, Zod or Joi, Valibot through the schema prop to validate the state. It's important to note that none of these libraries are included by default, so make sure to install the one you need.

Yup

<script setup lang="ts">
import { object, string, type InferType } from 'yup'
import type { FormSubmitEvent } from '#ui/types'

const schema = object({
  email: string().email('Invalid email').required('Required'),
  password: string()
    .min(8, 'Must be at least 8 characters')
    .required('Required')
})

type Schema = InferType<typeof schema>

const state = reactive({
  email: undefined,
  password: undefined
})

async function onSubmit (event: FormSubmitEvent<Schema>) {
  // Do something with event.data
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Zod

<script setup lang="ts">
import { z } from 'zod'
import type { FormSubmitEvent } from '#ui/types'

const schema = z.object({
  email: z.string().email('Invalid email'),
  password: z.string().min(8, 'Must be at least 8 characters')
})

type Schema = z.output<typeof schema>

const state = reactive({
  email: undefined,
  password: undefined
})

async function onSubmit (event: FormSubmitEvent<Schema>) {
  // Do something with data
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Joi

<script setup lang="ts">
import Joi from 'joi'
import type { FormSubmitEvent } from '#ui/types'

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

const state = reactive({
  email: undefined,
  password: undefined
})

async function onSubmit (event: FormSubmitEvent<any>) {
  // Do something with event.data
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Valibot

<script setup lang="ts">
import { string, objectAsync, email, minLength, type Input } from 'valibot'
import type { FormSubmitEvent } from '#ui/types'

const schema = objectAsync({
  email: string([email('Invalid email')]),
  password: string([minLength(8, 'Must be at least 8 characters')])
})

type Schema = Input<typeof schema>

const state = reactive({
  email: undefined,
  password: undefined
})

async function onSubmit (event: FormSubmitEvent<Schema>) {
  // Do something with event.data
  console.log(event.data)
}
</script>

<template>
  <UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Other libraries

For other validation libraries, you can define your own component with custom validation logic.

Here is an example with Vuelidate:

<script setup lang="ts">
import useVuelidate from '@vuelidate/core'

const props = defineProps({
  rules: { type: Object, required: true },
  model: { type: Object, required: true }
})

const form = ref();
const v = useVuelidate(props.rules, props.model)

async function validateWithVuelidate() {
  v.value.$touch()
  await v.value.$validate()
  return v.value.$errors.map((error) => ({
    message: error.$message,
    path: error.$propertyPath,
  }))
}

defineExpose({
  validate: async () => {
    await form.value.validate()
  }
})
</script>

<template>
  <UForm ref="form" :model="model" :validate="validateWithVuelidate">
    <slot />
  </UForm>
</template>

Backend validation

You can manually set errors after form submission if required. To do this, simply use the form.setErrors function to set the errors as needed.

<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '#ui/types'

const state = reactive({
  email: undefined,
  password: undefined
})

const form = ref()

async function onSubmit (event: FormSubmitEvent<any>) {
  form.value.clear()
  try {
    const response = await $fetch('...')
    // ...
  } catch (err) {
    if (err.statusCode === 422) {
      form.value.setErrors(err.data.errors.map((err) => {
        // Map validation errors to { path: string, message: string }
      }))
    }
  }
}
</script>

<template>
  <UForm ref="form" :state="state" @submit="onSubmit">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Input events

The Form component automatically triggers validation upon submit, input, blur or change events. This ensures that any errors are displayed as soon as the user interacts with the form elements. You can control when validation happens this using the validate-on prop.

Take a look at the component!

Error event New

You can listen to the @error event to handle errors. This event is triggered when the form is validated and contains an array of FormError objects with the following fields:

  • id - the identifier of the form element.
  • path - the path to the form element matching the name.
  • message - the error message to display.

Here is an example of how to focus the first form element with an error:

<script setup lang="ts">
import type { FormError, FormErrorEvent, FormSubmitEvent } from '#ui/types'

const state = reactive({
  email: undefined,
  password: undefined
})

const validate = (state: any): FormError[] => {
  const errors = []
  if (!state.email) errors.push({ path: 'email', message: 'Required' })
  if (!state.password) errors.push({ path: 'password', message: 'Required' })
  return errors
}

async function onSubmit (event: FormSubmitEvent<any>) {
  // Do something with data
  console.log(event.data)
}

async function onError (event: FormErrorEvent) {
  const element = document.getElementById(event.errors[0].id)
  element?.focus()
  element?.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
</script>

<template>
  <UForm :validate="validate" :state="state" class="space-y-4" @submit="onSubmit" @error="onError">
    <UFormGroup label="Email" name="email">
      <UInput v-model="state.email" />
    </UFormGroup>

    <UFormGroup label="Password" name="password">
      <UInput v-model="state.password" type="password" />
    </UFormGroup>

    <UButton type="submit">
      Submit
    </UButton>
  </UForm>
</template>

Props

staterequired
Record<string, any>
schema
ZodType<any, ZodTypeDef, any> | ObjectSchema<any, AnyObject, any, ""> | AnySchema<any> | ObjectSchemaAsync<any>
undefined
validate
((state: any) => Promise<FormError<string>[]>) | ((state: any) => FormError<string>[])
[]
validateOn
FormEventType[]
["blur", "input", "change", "submit"]

API

validate (path?: string, opts: { silent?: boolean })
Promise<T>

Triggers form validation. Will raise any errors unless opts.silent is set to true.

clear (path?: string)
void

Clears form errors associated with a specific path. If no path is provided, clears all form errors.

getErrors (path?: string)
FormError[]

Retrieves form errors associated with a specific path. If no path is provided, returns all form errors.

setErrors (errors: FormError[], path?: string)
void

Sets form errors for a given path. If no path is provided, overrides all errors.

errors
Ref<FormError[]>

A reference to the array containing validation errors. Use this to access or manipulate the error information.