This commit is contained in:
2026-09-27 11:51:31 +03:00
parent dfe330905c
commit ea5202f080
119 changed files with 79192 additions and 5 deletions
@@ -0,0 +1,235 @@
# Forms
## Basic pattern
Nuxt UI forms use `UForm` + `UFormField` + Standard Schema validation (Zod, Valibot, Yup, or Joi).
```vue
<script setup lang="ts">
import * as z from 'zod'
import type { FormSubmitEvent } from '@nuxt/ui'
const schema = z.object({
email: z.email('Invalid email'),
password: z.string().min(8, 'Min 8 characters')
})
type Schema = z.output<typeof schema>
const state = reactive<Partial<Schema>>({ email: '', password: '' })
function onSubmit(event: FormSubmitEvent<Schema>) {
// UForm validates before emitting @submit — access validated data via event.data
}
</script>
<template>
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
<UFormField name="email" label="Email" required>
<UInput v-model="state.email" type="email" placeholder="you@example.com" />
</UFormField>
<UFormField name="password" label="Password" required>
<UInput v-model="state.password" type="password" placeholder="Min 8 characters" />
</UFormField>
<UButton type="submit" label="Sign in" />
</UForm>
</template>
```
## Key rules
- Always use `UFormField` around inputs — it connects validation errors via the `name` prop
- The `name` prop on `UFormField` must match the schema field name exactly
- Use `reactive<Partial<Schema>>({})` for state — `Partial` allows empty initial values
- `@submit` only fires when validation passes
- For nested objects, use dot notation: `name="address.city"`
## UFormField props
| Prop | Purpose |
| ------------- | ------------------------------------------- |
| `name` | Links to schema field for validation errors |
| `label` | Visible label text |
| `description` | Help text below the input |
| `hint` | Right-aligned hint text (e.g., "Optional") |
| `required` | Shows required indicator |
| `size` | Inherits to child input |
## Field layout patterns
### Vertical stack (default)
```vue
<UForm :schema="schema" :state="state" class="space-y-4">
<UFormField name="name" label="Name">
<UInput v-model="state.name" />
</UFormField>
<UFormField name="email" label="Email">
<UInput v-model="state.email" />
</UFormField>
</UForm>
```
### Inline fields with UFieldGroup
```vue
<UFieldGroup>
<UFormField name="firstName" label="First name">
<UInput v-model="state.firstName" />
</UFormField>
<UFormField name="lastName" label="Last name">
<UInput v-model="state.lastName" />
</UFormField>
</UFieldGroup>
```
### Grid layout
```vue
<UForm :schema="schema" :state="state" class="grid grid-cols-2 gap-4">
<UFormField name="firstName" label="First name">
<UInput v-model="state.firstName" />
</UFormField>
<UFormField name="lastName" label="Last name">
<UInput v-model="state.lastName" />
</UFormField>
<UFormField name="email" label="Email" class="col-span-2">
<UInput v-model="state.email" type="email" />
</UFormField>
</UForm>
```
## Common field patterns
### Select
```vue
<UFormField name="role" label="Role">
<USelect v-model="state.role" :items="['Admin', 'Editor', 'Viewer']" placeholder="Choose role" />
</UFormField>
```
### Checkbox
```vue
<UFormField name="terms">
<UCheckbox v-model="state.terms" label="I agree to the terms and conditions" />
</UFormField>
```
### Radio group
```vue
<UFormField name="plan" label="Plan">
<URadioGroup
v-model="state.plan"
:items="[
{ label: 'Free', value: 'free', description: 'For personal projects' },
{ label: 'Pro', value: 'pro', description: 'For teams' }
]"
/>
</UFormField>
```
### Switch
```vue
<UFormField name="notifications" label="Email notifications">
<USwitch v-model="state.notifications" />
</UFormField>
```
### Textarea
```vue
<UFormField name="bio" label="Bio" description="Brief description for your profile.">
<UTextarea v-model="state.bio" :rows="3" autoresize :maxrows="6" />
</UFormField>
```
### File upload
```vue
<UFormField name="avatar" label="Avatar">
<UFileUpload v-model="state.avatar" accept="image/*" />
</UFormField>
<!-- Or as a drop area -->
<UFormField name="documents" label="Documents">
<UFileUpload v-model="state.documents" multiple variant="area" />
</UFormField>
```
### Date
```vue
<UFormField name="date" label="Date">
<UInputDate v-model="state.date" />
</UFormField>
<!-- Date range -->
<UFormField name="dateRange" label="Date range">
<UInputDate v-model="state.dateRange" range />
</UFormField>
```
## Programmatic validation
```vue
<script setup lang="ts">
const form = useTemplateRef('form')
async function validateAndSubmit() {
const result = await form.value?.validate({ silent: true })
if (result) {
// valid — submit
}
}
async function validateEmail() {
await form.value?.validate({ name: 'email', silent: true })
}
function setServerError() {
form.value?.setErrors([
{ name: 'email', message: 'Email already taken' }
])
}
function resetErrors() {
form.value?.clear()
}
</script>
<template>
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
<!-- fields -->
</UForm>
</template>
```
By default, `validate()` throws a `FormValidationException` when validation fails. Pass `{ silent: true }` when you want it to return `false` instead. Use `clear()` to remove validation errors.
## Form in a modal
Use `#footer="{ close }"` scoped slot for cancel/submit actions. Wrap the modal body in `UForm` with a `type="submit"` button in the footer so validation runs on submit.
```vue
<UModal v-model:open="isOpen" title="Edit profile" description="Update your information." :ui="{ footer: 'justify-end' }">
<template #body>
<UForm id="profile-form" :schema="schema" :state="state" class="space-y-4" @submit="onSave">
<UFormField name="name" label="Name">
<UInput v-model="state.name" />
</UFormField>
<UFormField name="email" label="Email">
<UInput v-model="state.email" type="email" />
</UFormField>
</UForm>
</template>
<template #footer="{ close }">
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
<UButton type="submit" form="profile-form" label="Save" />
</template>
</UModal>
```