docs: add form.md
- change TabPreview.vue to showcase way of using vee-validate
This commit is contained in:
parent
68337c5250
commit
854cb8f20c
|
|
@ -3,10 +3,14 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/lib/registry/default
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
name: string
|
name: string
|
||||||
|
names?: string[]
|
||||||
align?: 'center' | 'start' | 'end'
|
align?: 'center' | 'start' | 'end'
|
||||||
sfcTsCode?: string
|
sfcTsCode?: string
|
||||||
sfcTsHtml?: string
|
sfcTsHtml?: string
|
||||||
}>(), { align: 'center' })
|
}>(), {
|
||||||
|
align: 'center',
|
||||||
|
names: () => ['CLI', 'Manual'],
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -15,24 +19,17 @@ const props = withDefaults(defineProps<{
|
||||||
<div class="flex items-center justify-between pb-3">
|
<div class="flex items-center justify-between pb-3">
|
||||||
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0">
|
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="CLI"
|
v-for="(tab, index) in props.names"
|
||||||
|
:key="index"
|
||||||
|
:value="tab"
|
||||||
class="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
class="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
||||||
>
|
>
|
||||||
CLI
|
{{ tab }}
|
||||||
</TabsTrigger>
|
|
||||||
<TabsTrigger
|
|
||||||
value="Manual"
|
|
||||||
class="relative h-9 rounded-none border-b-2 border-b-transparent bg-transparent px-4 pb-3 pt-2 font-semibold text-muted-foreground shadow-none transition-none data-[state=active]:border-b-primary data-[state=active]:text-foreground data-[state=active]:shadow-none"
|
|
||||||
>
|
|
||||||
Manual
|
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
<TabsContent value="CLI" class="relative space-y-10">
|
<TabsContent v-for="(tab, index) in props.names" :key="index" :value="tab" class="relative space-y-10">
|
||||||
<slot name="CLI" />
|
<slot :name="tab" />
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="Manual">
|
|
||||||
<slot name="Manual" />
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,8 @@ export const docsConfig: DocsConfig = {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Form',
|
title: 'Form',
|
||||||
href: '#',
|
href: '/docs/components/form',
|
||||||
label: 'Soon',
|
label: 'New',
|
||||||
disabled: true,
|
|
||||||
items: [],
|
items: [],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
318
apps/www/src/content/docs/components/form.md
Normal file
318
apps/www/src/content/docs/components/form.md
Normal file
|
|
@ -0,0 +1,318 @@
|
||||||
|
---
|
||||||
|
title: VeeValidate
|
||||||
|
description: Building forms with VeeValidate and Zod.
|
||||||
|
---
|
||||||
|
|
||||||
|
Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex.
|
||||||
|
|
||||||
|
Well-designed HTML forms are:
|
||||||
|
|
||||||
|
- Well-structured and semantically correct.
|
||||||
|
- Easy to use and navigate (keyboard).
|
||||||
|
- Accessible with ARIA attributes and proper labels.
|
||||||
|
- Has support for client and server side validation.
|
||||||
|
- Well-styled and consistent with the rest of the application.
|
||||||
|
|
||||||
|
In this guide, we will take a look at building forms with [`vee-validate`](https://vee-validate.logaretm.com/v4/) and [`zod`](https://zod.dev). We're going to use a `<FormField>` component to compose accessible forms using Radix Vue components.
|
||||||
|
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
The `<Form />` component is a wrapper around the `vee-validate` library. It provides a few things:
|
||||||
|
|
||||||
|
|
||||||
|
- Composable components for building forms.
|
||||||
|
- A `<FormField />` component for building controlled form fields.
|
||||||
|
- Form validation using `zod`.
|
||||||
|
- Applies the correct `aria` attributes to form fields based on states, handle unqiue IDs
|
||||||
|
- Built to work with all Radix Vue components.
|
||||||
|
- Bring your own schema library. We use `zod` but you can use any other supported schema validation you want, like [`yup`](https://github.com/jquense/yup) or [`valibot`](https://valibot.dev/).
|
||||||
|
- **You have full control over the markup and styling.**
|
||||||
|
|
||||||
|
[`vee-validate`](https://vee-validate.logaretm.com/v4/) makes use of two flavors to add validation to your forms.
|
||||||
|
- Composition API
|
||||||
|
- Higher-order components (HOC)
|
||||||
|
|
||||||
|
## Anatomy
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<Form>
|
||||||
|
<FormField v-slot="{ ... }">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel />
|
||||||
|
<FormControl>
|
||||||
|
<!-- any Form Input component or native input elements -->
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription />
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
</Form>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
|
||||||
|
<TabPreview name="Component" :names="['Component', 'Native']">
|
||||||
|
<template #Component>
|
||||||
|
|
||||||
|
#### Input Component
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<FormField v-slot="{ componentField }">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="shadcn" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription />
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
```
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #Native>
|
||||||
|
|
||||||
|
#### Native input element
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<FormField v-slot="{ field }">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<input placeholder="shadcn" v-bind="field" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription />
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
```
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</TabPreview>
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
<TabPreview name="CLI">
|
||||||
|
<template #CLI>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn-vue@latest add form
|
||||||
|
```
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #Manual>
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
### Install the following dependency:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install radix-vue vee-validate @vee-validate/zod zod
|
||||||
|
```
|
||||||
|
|
||||||
|
### Copy and paste the following codes into your project:
|
||||||
|
|
||||||
|
`index.ts`
|
||||||
|
|
||||||
|
<<< @/lib/registry/default/ui/form/index.ts
|
||||||
|
|
||||||
|
`FormItem.vue`
|
||||||
|
|
||||||
|
<<< @/lib/registry/default/ui/form/FormItem.vue
|
||||||
|
|
||||||
|
`FormLabel.vue`
|
||||||
|
|
||||||
|
<<< @/lib/registry/default/ui/form/FormLabel.vue
|
||||||
|
|
||||||
|
`FormControl.vue`
|
||||||
|
|
||||||
|
<<< @/lib/registry/default/ui/form/FormControl.vue
|
||||||
|
|
||||||
|
`FormMessage.vue`
|
||||||
|
|
||||||
|
<<< @/lib/registry/default/ui/form/FormMessage.vue
|
||||||
|
|
||||||
|
`FormDescription.vue`
|
||||||
|
|
||||||
|
<<< @/lib/registry/default/ui/form/FormDescription.vue
|
||||||
|
|
||||||
|
### Update the import paths to match your project setup.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</TabPreview>
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
### Create a form schema
|
||||||
|
|
||||||
|
Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).
|
||||||
|
|
||||||
|
Use `@vee-validate/zod` to integrate Zod schema validation with `vee-validate`
|
||||||
|
|
||||||
|
`toTypedSchema` also makes the form values and submitted values typed automatically and caters for both input and output types of that schema.
|
||||||
|
|
||||||
|
```vue showLineNumbers {2-3,5-7}
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(z.object({
|
||||||
|
username: z.string().min(2).max(50),
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Define a form
|
||||||
|
|
||||||
|
Use the `useForm` composable from `vee-validate` or use `<Form />` component to create a from.
|
||||||
|
|
||||||
|
|
||||||
|
<TabPreview name="Composition" :names="['Composition', 'Component']">
|
||||||
|
<template #Composition>
|
||||||
|
|
||||||
|
```vue showLineNumbers {2,19-21}
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(z.object({
|
||||||
|
username: z.string().min(2).max(50),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
validationSchema: formSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((values) => {
|
||||||
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit="onSubmit">
|
||||||
|
...
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #Component>
|
||||||
|
|
||||||
|
```vue showLineNumbers {5,24-26}
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(z.object({
|
||||||
|
username: z.string().min(2).max(50),
|
||||||
|
}))
|
||||||
|
|
||||||
|
function onSubmit(values) {
|
||||||
|
console.log('Form submitted!', values)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Form :validation-schema="formSchema" @submit="onSubmit">
|
||||||
|
...
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
</template>
|
||||||
|
</TabPreview>
|
||||||
|
|
||||||
|
### Build your form
|
||||||
|
|
||||||
|
Based on last step we can either use `<Form />` component or `useForm` composable
|
||||||
|
`useForm` is recommended cause values are typed automatically
|
||||||
|
|
||||||
|
```vue showLineNumbers {2}
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/components/ui/form'
|
||||||
|
import { Input } from '@/components/ui/input'
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(z.object({
|
||||||
|
username: z.string().min(2).max(50),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
validationSchema: formSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit((values) => {
|
||||||
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form @submit="onSubmit">
|
||||||
|
<FormField v-slot="{ componentField }" name="username">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="text" placeholder="shadcn" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is your public display name.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
<Button type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Done
|
||||||
|
|
||||||
|
That's it. You now have a fully accessible form that is type-safe with client-side validation.
|
||||||
|
|
||||||
|
<ComponentPreview
|
||||||
|
name="InputForm"
|
||||||
|
className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</Steps>
|
||||||
48
apps/www/src/lib/registry/default/example/InputForm.vue
Normal file
48
apps/www/src/lib/registry/default/example/InputForm.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import { Button } from '@/lib/registry/default/ui/button'
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/lib/registry/default/ui/form'
|
||||||
|
import { Input } from '@/lib/registry/default/ui/input'
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(z.object({
|
||||||
|
username: z.string().min(2).max(50),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { handleSubmit } = useForm({
|
||||||
|
validationSchema: formSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((values) => {
|
||||||
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="w-2/3 space-y-6" @submit="onSubmit">
|
||||||
|
<FormField v-slot="{ componentField }" name="username">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="text" placeholder="shadcn" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is your public display name.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
<Button type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
48
apps/www/src/lib/registry/new-york/example/InputForm.vue
Normal file
48
apps/www/src/lib/registry/new-york/example/InputForm.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useForm } from 'vee-validate'
|
||||||
|
import { toTypedSchema } from '@vee-validate/zod'
|
||||||
|
import * as z from 'zod'
|
||||||
|
|
||||||
|
import { Button } from '@/lib/registry/new-york/ui/button'
|
||||||
|
import {
|
||||||
|
FormControl,
|
||||||
|
FormDescription,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from '@/lib/registry/new-york/ui/form'
|
||||||
|
import { Input } from '@/lib/registry/new-york/ui/input'
|
||||||
|
|
||||||
|
const formSchema = toTypedSchema(z.object({
|
||||||
|
username: z.string().min(2).max(50),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { isFieldDirty, handleSubmit } = useForm({
|
||||||
|
validationSchema: formSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const onSubmit = handleSubmit((values) => {
|
||||||
|
console.log('Form submitted!', values)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<form class="w-2/3 space-y-6" @submit="onSubmit">
|
||||||
|
<FormField v-slot="{ componentField }" name="username" :validate-on-blur="!isFieldDirty">
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Username</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input type="text" placeholder="shadcn" v-bind="componentField" />
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
This is your public display name.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
</FormField>
|
||||||
|
<Button type="submit">
|
||||||
|
Submit
|
||||||
|
</Button>
|
||||||
|
</Form>
|
||||||
|
</template>
|
||||||
Loading…
Reference in New Issue
Block a user