mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-11 09:51:40 +00:00
feat: react-hook-form (#377)
* feat(form): add form component * feat(www): update site styles * feat: add form examples * docs(www): add docs for forms * docs(www): hide tabs for docs demo
This commit is contained in:
@@ -82,7 +82,7 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) {
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
|
||||
218
apps/www/app/examples/forms/account/account-form.tsx
Normal file
218
apps/www/app/examples/forms/account/account-form.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon, Check, ChevronsUpDown } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const languages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "Portuguese", value: "pt" },
|
||||
{ label: "Russian", value: "ru" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
{ label: "Korean", value: "ko" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
] as const
|
||||
|
||||
const accountFormSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Name must be at least 2 characters.",
|
||||
})
|
||||
.max(30, {
|
||||
message: "Name must not be longer than 30 characters.",
|
||||
}),
|
||||
dob: z.date({
|
||||
required_error: "A date of birth is required.",
|
||||
}),
|
||||
language: z.string({
|
||||
required_error: "Please select a language.",
|
||||
}),
|
||||
})
|
||||
|
||||
type AccountFormValues = z.infer<typeof accountFormSchema>
|
||||
|
||||
// This can come from your database or API.
|
||||
const defaultValues: Partial<AccountFormValues> = {
|
||||
// name: "Your name",
|
||||
// dob: new Date("2023-01-23"),
|
||||
}
|
||||
|
||||
export function AccountForm() {
|
||||
const form = useForm<AccountFormValues>({
|
||||
resolver: zodResolver(accountFormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
function onSubmit(data: AccountFormValues) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Your name" {...form.register("name")} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is the name that will be displayed on your profile and in
|
||||
emails.
|
||||
</FormDescription>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dob"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date of birth</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-[240px] pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Your date of birth is used to calculate your age.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Language</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-[200px] justify-between",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? languages.find(
|
||||
(language) => language.value === field.value
|
||||
)?.label
|
||||
: "Select language"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.value}
|
||||
key={language.value}
|
||||
onSelect={(value) => {
|
||||
form.setValue("language", value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
language.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
This is the language that will be used in the dashboard.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Update account</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
18
apps/www/app/examples/forms/account/page.tsx
Normal file
18
apps/www/app/examples/forms/account/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { AccountForm } from "@/app/examples/forms/account/account-form"
|
||||
|
||||
export default function SettingsAccountPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Account</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Update your account settings. Set your preferred language and
|
||||
timezone.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<AccountForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
164
apps/www/app/examples/forms/appearance/appearance-form.tsx
Normal file
164
apps/www/app/examples/forms/appearance/appearance-form.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const appearanceFormSchema = z.object({
|
||||
theme: z.enum(["light", "dark"], {
|
||||
required_error: "Please select a theme.",
|
||||
}),
|
||||
font: z.enum(["inter", "manrope", "system"], {
|
||||
invalid_type_error: "Select a font",
|
||||
required_error: "Please select a font.",
|
||||
}),
|
||||
})
|
||||
|
||||
type AppearanceFormValues = z.infer<typeof appearanceFormSchema>
|
||||
|
||||
// This can come from your database or API.
|
||||
const defaultValues: Partial<AppearanceFormValues> = {
|
||||
theme: "light",
|
||||
}
|
||||
|
||||
export function AppearanceForm() {
|
||||
const form = useForm<AppearanceFormValues>({
|
||||
resolver: zodResolver(appearanceFormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
function onSubmit(data: AppearanceFormValues) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="font"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Font</FormLabel>
|
||||
<div className="relative w-max">
|
||||
<FormControl>
|
||||
<select
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"w-[200px] appearance-none bg-transparent font-normal"
|
||||
)}
|
||||
{...form.register("font")}
|
||||
>
|
||||
<option value="inter">Inter</option>
|
||||
<option value="manrope">Manrope</option>
|
||||
<option value="system">System</option>
|
||||
</select>
|
||||
</FormControl>
|
||||
<ChevronDown className="absolute right-3 top-3 h-4 w-4 opacity-50" />
|
||||
</div>
|
||||
<FormDescription>
|
||||
Set the font you want to use in the dashboard.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="theme"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-1">
|
||||
<FormLabel>Theme</FormLabel>
|
||||
<FormDescription>
|
||||
Select the theme for the dashboard.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="grid max-w-md grid-cols-2 gap-8 pt-2"
|
||||
>
|
||||
<FormItem>
|
||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="light" className="sr-only" />
|
||||
</FormControl>
|
||||
<div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
||||
<div className="space-y-2 rounded-sm bg-[#ecedef] p-2">
|
||||
<div className="space-y-2 rounded-md bg-white p-2 shadow-sm">
|
||||
<div className="h-2 w-[80px] rounded-lg bg-[#ecedef]" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-white p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-[#ecedef]" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-[#ecedef]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="block w-full p-2 text-center font-normal">
|
||||
Light
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<FormLabel className="[&:has([data-state=checked])>div]:border-primary">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="dark" className="sr-only" />
|
||||
</FormControl>
|
||||
<div className="items-center rounded-md border-2 border-muted bg-popover p-1 hover:bg-accent hover:text-accent-foreground">
|
||||
<div className="space-y-2 rounded-sm bg-slate-950 p-2">
|
||||
<div className="space-y-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||
<div className="h-2 w-[80px] rounded-lg bg-slate-400" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-slate-400" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2 rounded-md bg-slate-800 p-2 shadow-sm">
|
||||
<div className="h-4 w-4 rounded-full bg-slate-400" />
|
||||
<div className="h-2 w-[100px] rounded-lg bg-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span className="block w-full p-2 text-center font-normal">
|
||||
Dark
|
||||
</span>
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Update preferences</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
18
apps/www/app/examples/forms/appearance/page.tsx
Normal file
18
apps/www/app/examples/forms/appearance/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { AppearanceForm } from "@/app/examples/forms/appearance/appearance-form"
|
||||
|
||||
export default function SettingsAppearancePage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Appearance</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Customize the appearance of the app. Automatically switch between day
|
||||
and night themes.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<AppearanceForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
44
apps/www/app/examples/forms/components/sidebar-nav.tsx
Normal file
44
apps/www/app/examples/forms/components/sidebar-nav.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
|
||||
items: {
|
||||
href: string
|
||||
title: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav
|
||||
className={cn(
|
||||
"flex space-x-2 lg:flex-col lg:space-x-0 lg:space-y-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
pathname === item.href
|
||||
? "bg-muted hover:bg-muted"
|
||||
: "hover:bg-transparent hover:underline",
|
||||
"justify-start"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
132
apps/www/app/examples/forms/display/display-form.tsx
Normal file
132
apps/www/app/examples/forms/display/display-form.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: "recents",
|
||||
label: "Recents",
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
label: "Home",
|
||||
},
|
||||
{
|
||||
id: "applications",
|
||||
label: "Applications",
|
||||
},
|
||||
{
|
||||
id: "desktop",
|
||||
label: "Desktop",
|
||||
},
|
||||
{
|
||||
id: "downloads",
|
||||
label: "Downloads",
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents",
|
||||
},
|
||||
] as const
|
||||
|
||||
const displayFormSchema = z.object({
|
||||
items: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
message: "You have to select at least one item.",
|
||||
}),
|
||||
})
|
||||
|
||||
type DisplayFormValues = z.infer<typeof displayFormSchema>
|
||||
|
||||
// This can come from your database or API.
|
||||
const defaultValues: Partial<DisplayFormValues> = {
|
||||
items: ["recents", "home"],
|
||||
}
|
||||
|
||||
export function DisplayForm() {
|
||||
const form = useForm<DisplayFormValues>({
|
||||
resolver: zodResolver(displayFormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
function onSubmit(data: DisplayFormValues) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="items"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<div className="mb-4">
|
||||
<FormLabel className="text-base">Sidebar</FormLabel>
|
||||
<FormDescription>
|
||||
Select the items you want to display in the sidebar.
|
||||
</FormDescription>
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name="items"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, item.id])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== item.id
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{item.label}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Update display</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
17
apps/www/app/examples/forms/display/page.tsx
Normal file
17
apps/www/app/examples/forms/display/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { DisplayForm } from "@/app/examples/forms/display/display-form"
|
||||
|
||||
export default function SettingsDisplayPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Display</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Turn items on or off to control what's displayed in the app.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<DisplayForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
75
apps/www/app/examples/forms/layout.tsx
Normal file
75
apps/www/app/examples/forms/layout.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { SidebarNav } from "@/app/examples/forms/components/sidebar-nav"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Forms",
|
||||
description: "Advanced form example using react-hook-form and Zod.",
|
||||
}
|
||||
|
||||
const sidebarNavItems = [
|
||||
{
|
||||
title: "Profile",
|
||||
href: "/examples/forms",
|
||||
},
|
||||
{
|
||||
title: "Account",
|
||||
href: "/examples/forms/account",
|
||||
},
|
||||
{
|
||||
title: "Appearance",
|
||||
href: "/examples/forms/appearance",
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
href: "/examples/forms/notifications",
|
||||
},
|
||||
{
|
||||
title: "Display",
|
||||
href: "/examples/forms/display",
|
||||
},
|
||||
]
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function SettingsLayout({ children }: SettingsLayoutProps) {
|
||||
return (
|
||||
<>
|
||||
<div className="md:hidden">
|
||||
<Image
|
||||
src="/examples/forms-light.png"
|
||||
width={1280}
|
||||
height={791}
|
||||
alt="Forms"
|
||||
className="block dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/examples/forms-dark.png"
|
||||
width={1280}
|
||||
height={791}
|
||||
alt="Forms"
|
||||
className="hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden space-y-6 p-10 pb-16 md:block">
|
||||
<div className="space-y-0.5">
|
||||
<h2 className="text-2xl font-bold tracking-tight">Settings</h2>
|
||||
<p className="text-muted-foreground">
|
||||
Manage your account settings and set e-mail preferences.
|
||||
</p>
|
||||
</div>
|
||||
<Separator className="my-6" />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0">
|
||||
<aside className="-mx-4 lg:w-1/5">
|
||||
<SidebarNav items={sidebarNavItems} />
|
||||
</aside>
|
||||
<div className="flex-1 lg:max-w-2xl">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
222
apps/www/app/examples/forms/notifications/notifications-form.tsx
Normal file
222
apps/www/app/examples/forms/notifications/notifications-form.tsx
Normal file
@@ -0,0 +1,222 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const notificationsFormSchema = z.object({
|
||||
type: z.enum(["all", "mentions", "none"], {
|
||||
required_error: "You need to select a notification type.",
|
||||
}),
|
||||
mobile: z.boolean().default(false).optional(),
|
||||
communication_emails: z.boolean().default(false).optional(),
|
||||
social_emails: z.boolean().default(false).optional(),
|
||||
marketing_emails: z.boolean().default(false).optional(),
|
||||
security_emails: z.boolean(),
|
||||
})
|
||||
|
||||
type NotificationsFormValues = z.infer<typeof notificationsFormSchema>
|
||||
|
||||
// This can come from your database or API.
|
||||
const defaultValues: Partial<NotificationsFormValues> = {
|
||||
communication_emails: false,
|
||||
marketing_emails: false,
|
||||
social_emails: true,
|
||||
security_emails: true,
|
||||
}
|
||||
|
||||
export function NotificationsForm() {
|
||||
const form = useForm<NotificationsFormValues>({
|
||||
resolver: zodResolver(notificationsFormSchema),
|
||||
defaultValues,
|
||||
})
|
||||
|
||||
function onSubmit(data: NotificationsFormValues) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Notify me about...</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="all" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
All new messages
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="mentions" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Direct messages and mentions
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="none" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Nothing</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">Email Notifications</h3>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="communication_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Communication emails
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Receive emails about your account activity.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="marketing_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Marketing emails
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Receive emails about new products, features, and more.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="social_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Social emails</FormLabel>
|
||||
<FormDescription>
|
||||
Receive emails for friend requests, follows, and more.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="security_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Security emails</FormLabel>
|
||||
<FormDescription>
|
||||
Receive emails about your account activity and security.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled
|
||||
aria-readonly
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mobile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
Use different settings for my mobile devices
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
You can manage your mobile notifications in the{" "}
|
||||
<Link href="/examples/forms">mobile settings</Link> page.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Update notifications</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
18
apps/www/app/examples/forms/notifications/page.tsx
Normal file
18
apps/www/app/examples/forms/notifications/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { AccountForm } from "@/app/examples/forms/account/account-form"
|
||||
import { NotificationsForm } from "@/app/examples/forms/notifications/notifications-form"
|
||||
|
||||
export default function SettingsNotificationsPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Notifications</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure how you receive notifications.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<NotificationsForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
17
apps/www/app/examples/forms/page.tsx
Normal file
17
apps/www/app/examples/forms/page.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import { ProfileForm } from "@/app/examples/forms/profile-form"
|
||||
|
||||
export default function SettingsProfilePage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Profile</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is how others will see you on the site.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<ProfileForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
191
apps/www/app/examples/forms/profile-form.tsx
Normal file
191
apps/www/app/examples/forms/profile-form.tsx
Normal file
@@ -0,0 +1,191 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useFieldArray, useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const profileFormSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
})
|
||||
.max(30, {
|
||||
message: "Username must not be longer than 30 characters.",
|
||||
}),
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Please select an email to display.",
|
||||
})
|
||||
.email(),
|
||||
bio: z.string().max(160).min(4),
|
||||
urls: z
|
||||
.array(
|
||||
z.object({
|
||||
value: z.string().url({ message: "Please enter a valid URL." }),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
})
|
||||
|
||||
type ProfileFormValues = z.infer<typeof profileFormSchema>
|
||||
|
||||
// This can come from your database or API.
|
||||
const defaultValues: Partial<ProfileFormValues> = {
|
||||
bio: "I own a computer.",
|
||||
urls: [
|
||||
{ value: "https://shadcn.com" },
|
||||
{ value: "http://twitter.com/shadcn" },
|
||||
],
|
||||
}
|
||||
|
||||
export function ProfileForm() {
|
||||
const form = useForm<ProfileFormValues>({
|
||||
resolver: zodResolver(profileFormSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
})
|
||||
|
||||
const { fields, append } = useFieldArray({
|
||||
name: "urls",
|
||||
control: form.control,
|
||||
})
|
||||
|
||||
function onSubmit(data: ProfileFormValues) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...form.register("username")} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name. It can be your real name or a
|
||||
pseudonym. You can only change this once every 30 days.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a verified email to display" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="m@example.com">m@example.com</SelectItem>
|
||||
<SelectItem value="m@google.com">m@google.com</SelectItem>
|
||||
<SelectItem value="m@support.com">m@support.com</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
You can manage verified email addresses in your{" "}
|
||||
<Link href="/examples/forms">email settings</Link>.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bio"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
className="resize-none"
|
||||
{...form.register("bio")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can <span>@mention</span> other users and organizations to
|
||||
link to them.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div>
|
||||
{fields.map((field, index) => (
|
||||
<FormField
|
||||
control={form.control}
|
||||
key={field.id}
|
||||
name={`urls.${index}`}
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel className={cn(index !== 0 && "sr-only")}>
|
||||
URLs
|
||||
</FormLabel>
|
||||
<FormDescription className={cn(index !== 0 && "sr-only")}>
|
||||
Add links to your website, blog, or social media profiles.
|
||||
</FormDescription>
|
||||
<FormControl>
|
||||
<Input {...form.register(`urls.${index}.value`)} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
onClick={() => append({ value: "" })}
|
||||
>
|
||||
Add URL
|
||||
</Button>
|
||||
</div>
|
||||
<Button type="submit">Update profile</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -27,11 +27,11 @@ export default function ExamplesLayout({ children }: ExamplesLayoutProps) {
|
||||
<div className="container relative pb-10">
|
||||
<PageHeader className="page-header">
|
||||
<Link
|
||||
href="/docs/components/data-table"
|
||||
href="/docs/forms/react-hook-form"
|
||||
className="inline-flex items-center rounded-lg bg-muted px-3 py-1 text-sm font-medium"
|
||||
>
|
||||
🎉 <Separator className="mx-2 h-4" orientation="vertical" />{" "}
|
||||
Introducing Table and Data Table{" "}
|
||||
Building forms with React Hook Form and Zod
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
<PageHeaderHeading className="hidden md:block">
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
PageHeaderDescription,
|
||||
PageHeaderHeading,
|
||||
} from "@/components/page-header"
|
||||
import { PromoVideo } from "@/components/promo-video"
|
||||
import { StyleSwitcher } from "@/components/style-switcher"
|
||||
import DashboardPage from "@/app/examples/dashboard/page"
|
||||
|
||||
@@ -23,11 +22,11 @@ export default function IndexPage() {
|
||||
<StyleSwitcher />
|
||||
<PageHeader>
|
||||
<Link
|
||||
href="/docs/components/data-table"
|
||||
href="/docs/forms/react-hook-form"
|
||||
className="inline-flex items-center rounded-lg bg-muted px-3 py-1 text-sm font-medium"
|
||||
>
|
||||
🎉 <Separator className="mx-2 h-4" orientation="vertical" />{" "}
|
||||
Introducing Table and Data Table{" "}
|
||||
🎉 <Separator className="mx-2 h-4" orientation="vertical" /> Building
|
||||
forms with React Hook Form and Zod
|
||||
<ChevronRight className="ml-1 h-4 w-4" />
|
||||
</Link>
|
||||
<PageHeaderHeading>Build your component library.</PageHeaderHeading>
|
||||
|
||||
@@ -24,6 +24,11 @@ const examples = [
|
||||
name: "Playground",
|
||||
href: "/examples/playground",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/examples/forms",
|
||||
label: "New",
|
||||
},
|
||||
{
|
||||
name: "Music",
|
||||
href: "/examples/music",
|
||||
@@ -48,7 +53,7 @@ export function ExamplesNav({ className, ...props }: ExamplesNavProps) {
|
||||
key={example.href}
|
||||
className={cn(
|
||||
"flex items-center px-4",
|
||||
pathname === example.href
|
||||
pathname?.startsWith(example.href)
|
||||
? "font-bold text-primary"
|
||||
: "font-medium text-muted-foreground"
|
||||
)}
|
||||
|
||||
101
apps/www/components/examples/calendar/react-hook-form.tsx
Normal file
101
apps/www/components/examples/calendar/react-hook-form.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
dob: z.date({
|
||||
required_error: "A date of birth is required.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function CalendarReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dob"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date of birth</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-[240px] pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Your date of birth is used to calculate your age.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: "recents",
|
||||
label: "Recents",
|
||||
},
|
||||
{
|
||||
id: "home",
|
||||
label: "Home",
|
||||
},
|
||||
{
|
||||
id: "applications",
|
||||
label: "Applications",
|
||||
},
|
||||
{
|
||||
id: "desktop",
|
||||
label: "Desktop",
|
||||
},
|
||||
{
|
||||
id: "downloads",
|
||||
label: "Downloads",
|
||||
},
|
||||
{
|
||||
id: "documents",
|
||||
label: "Documents",
|
||||
},
|
||||
] as const
|
||||
|
||||
const FormSchema = z.object({
|
||||
items: z.array(z.string()).refine((value) => value.some((item) => item), {
|
||||
message: "You have to select at least one item.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function CheckboxReactHookFormMultiple() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
items: ["recents", "home"],
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="items"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<div className="mb-4">
|
||||
<FormLabel className="text-base">Sidebar</FormLabel>
|
||||
<FormDescription>
|
||||
Select the items you want to display in the sidebar.
|
||||
</FormDescription>
|
||||
</div>
|
||||
{items.map((item) => (
|
||||
<FormField
|
||||
key={item.id}
|
||||
control={form.control}
|
||||
name="items"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem
|
||||
key={item.id}
|
||||
className="flex flex-row items-start space-x-3 space-y-0"
|
||||
>
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value?.includes(item.id)}
|
||||
onCheckedChange={(checked) => {
|
||||
return checked
|
||||
? field.onChange([...field.value, item.id])
|
||||
: field.onChange(
|
||||
field.value?.filter(
|
||||
(value) => value !== item.id
|
||||
)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
{item.label}
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
mobile: z.boolean().default(false).optional(),
|
||||
})
|
||||
|
||||
export function CheckboxReactHookFormSingle() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
mobile: true,
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mobile"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
|
||||
<FormControl>
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<div className="space-y-1 leading-none">
|
||||
<FormLabel>
|
||||
Use different settings for my mobile devices
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
You can manage your mobile notifications in the{" "}
|
||||
<Link href="/examples/forms">mobile settings</Link> page.
|
||||
</FormDescription>
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
135
apps/www/components/examples/combobox/react-hook-form.tsx
Normal file
135
apps/www/components/examples/combobox/react-hook-form.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Check, ChevronsUpDown } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
} from "@/components/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const languages = [
|
||||
{ label: "English", value: "en" },
|
||||
{ label: "French", value: "fr" },
|
||||
{ label: "German", value: "de" },
|
||||
{ label: "Spanish", value: "es" },
|
||||
{ label: "Portuguese", value: "pt" },
|
||||
{ label: "Russian", value: "ru" },
|
||||
{ label: "Japanese", value: "ja" },
|
||||
{ label: "Korean", value: "ko" },
|
||||
{ label: "Chinese", value: "zh" },
|
||||
] as const
|
||||
|
||||
const FormSchema = z.object({
|
||||
language: z.string({
|
||||
required_error: "Please select a language.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function ComboboxReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="language"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Language</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
className={cn(
|
||||
"w-[200px] justify-between",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value
|
||||
? languages.find(
|
||||
(language) => language.value === field.value
|
||||
)?.label
|
||||
: "Select language"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{languages.map((language) => (
|
||||
<CommandItem
|
||||
value={language.value}
|
||||
key={language.value}
|
||||
onSelect={(value) => {
|
||||
form.setValue("language", value)
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
language.value === field.value
|
||||
? "opacity-100"
|
||||
: "opacity-0"
|
||||
)}
|
||||
/>
|
||||
{language.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
This is the language that will be used in the dashboard.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
101
apps/www/components/examples/date-picker/react-hook-form.tsx
Normal file
101
apps/www/components/examples/date-picker/react-hook-form.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { format } from "date-fns"
|
||||
import { CalendarIcon } from "lucide-react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Calendar } from "@/components/ui/calendar"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
dob: z.date({
|
||||
required_error: "A date of birth is required.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function DatePickerReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dob"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<FormLabel>Date of birth</FormLabel>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-[240px] pl-3 text-left font-normal",
|
||||
!field.value && "text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{field.value ? (
|
||||
format(field.value, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value}
|
||||
onSelect={field.onChange}
|
||||
disabled={(date) =>
|
||||
date > new Date() || date < new Date("1900-01-01")
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<FormDescription>
|
||||
Your date of birth is used to calculate your age.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -18,20 +18,25 @@ import { ButtonOutline } from "@/components/examples/button/outline"
|
||||
import { ButtonSecondary } from "@/components/examples/button/secondary"
|
||||
import { ButtonWithIcon } from "@/components/examples/button/with-icon"
|
||||
import { CalendarDemo } from "@/components/examples/calendar/demo"
|
||||
import { CalendarReactHookForm } from "@/components/examples/calendar/react-hook-form"
|
||||
import { CardDemo } from "@/components/examples/card/demo"
|
||||
import { CardWithForm } from "@/components/examples/card/with-form"
|
||||
import { CheckboxDemo } from "@/components/examples/checkbox/demo"
|
||||
import { CheckboxDisabled } from "@/components/examples/checkbox/disabled"
|
||||
import { CheckboxReactHookFormMultiple } from "@/components/examples/checkbox/react-hook-form-multiple"
|
||||
import { CheckboxReactHookFormSingle } from "@/components/examples/checkbox/react-hook-form-single"
|
||||
import { CheckboxWithText } from "@/components/examples/checkbox/with-text"
|
||||
import { CollapsibleDemo } from "@/components/examples/collapsible/demo"
|
||||
import { ComboboxDemo } from "@/components/examples/combobox/demo"
|
||||
import { ComboboxDropdownMenu } from "@/components/examples/combobox/dropdown-menu"
|
||||
import { ComboboxPopover } from "@/components/examples/combobox/popover"
|
||||
import { ComboboxReactHookForm } from "@/components/examples/combobox/react-hook-form"
|
||||
import { CommandDemo } from "@/components/examples/command/demo"
|
||||
import { CommandDialogDemo } from "@/components/examples/command/dialog"
|
||||
import { ContextMenuDemo } from "@/components/examples/context-menu/demo"
|
||||
import { DataTableDemo } from "@/components/examples/data-table/demo"
|
||||
import { DatePickerDemo } from "@/components/examples/date-picker/demo"
|
||||
import { DatePickerReactHookForm } from "@/components/examples/date-picker/react-hook-form"
|
||||
import { DatePickerWithPresets } from "@/components/examples/date-picker/with-presets"
|
||||
import { DatePickerWithRange } from "@/components/examples/date-picker/with-range"
|
||||
import { DialogDemo } from "@/components/examples/dialog/demo"
|
||||
@@ -42,6 +47,7 @@ import { HoverCardDemo } from "@/components/examples/hover-card/demo"
|
||||
import { InputDemo } from "@/components/examples/input/demo"
|
||||
import { InputDisabled } from "@/components/examples/input/disabled"
|
||||
import { InputFile } from "@/components/examples/input/file"
|
||||
import { InputReactHookForm } from "@/components/examples/input/react-hook-form"
|
||||
import { InputWithButton } from "@/components/examples/input/with-button"
|
||||
import { InputWithLabel } from "@/components/examples/input/with-label"
|
||||
import { InputWithText } from "@/components/examples/input/with-text"
|
||||
@@ -51,8 +57,10 @@ import { NavigationMenuDemo } from "@/components/examples/navigation-menu/demo"
|
||||
import { PopoverDemo } from "@/components/examples/popover/demo"
|
||||
import { ProgressDemo } from "@/components/examples/progress/demo"
|
||||
import { RadioGroupDemo } from "@/components/examples/radio-group/demo"
|
||||
import { RadioGroupReactHookForm } from "@/components/examples/radio-group/react-hook-form"
|
||||
import { ScrollAreaDemo } from "@/components/examples/scroll-area/demo"
|
||||
import { SelectDemo } from "@/components/examples/select/demo"
|
||||
import { SelectReactHookForm } from "@/components/examples/select/react-hook-form"
|
||||
import { SeparatorDemo } from "@/components/examples/separator/demo"
|
||||
import { SheetDemo } from "@/components/examples/sheet/demo"
|
||||
import { SheetPosition } from "@/components/examples/sheet/position"
|
||||
@@ -60,10 +68,12 @@ import { SheetSize } from "@/components/examples/sheet/size"
|
||||
import { SkeletonDemo } from "@/components/examples/skeleton/demo"
|
||||
import { SliderDemo } from "@/components/examples/slider/demo"
|
||||
import { SwitchDemo } from "@/components/examples/switch/demo"
|
||||
import { SwitchReactHookForm } from "@/components/examples/switch/react-hook-form"
|
||||
import { TableDemo } from "@/components/examples/table/demo"
|
||||
import { TabsDemo } from "@/components/examples/tabs/demo"
|
||||
import { TextareaDemo } from "@/components/examples/textarea/demo"
|
||||
import { TextareaDisabled } from "@/components/examples/textarea/disabled"
|
||||
import { TextareaReactHookForm } from "@/components/examples/textarea/react-hook-form"
|
||||
import { TextareaWithButton } from "@/components/examples/textarea/with-button"
|
||||
import { TextareaWithLabel } from "@/components/examples/textarea/with-label"
|
||||
import { TextareaWithText } from "@/components/examples/textarea/with-text"
|
||||
@@ -115,21 +125,26 @@ export const examples = {
|
||||
ButtonWithIcon,
|
||||
ButtonAsChild,
|
||||
CalendarDemo,
|
||||
CalendarReactHookForm,
|
||||
DataTableDemo,
|
||||
DatePickerDemo,
|
||||
DatePickerWithRange,
|
||||
DatePickerWithPresets,
|
||||
DatePickerReactHookForm,
|
||||
CardDemo,
|
||||
CardWithForm,
|
||||
CheckboxDemo,
|
||||
CheckboxDisabled,
|
||||
CheckboxWithText,
|
||||
CheckboxReactHookFormMultiple,
|
||||
CheckboxReactHookFormSingle,
|
||||
CollapsibleDemo,
|
||||
CommandDemo,
|
||||
CommandDialogDemo,
|
||||
ComboboxDemo,
|
||||
ComboboxPopover,
|
||||
ComboboxDropdownMenu,
|
||||
ComboboxReactHookForm,
|
||||
ContextMenuDemo,
|
||||
DialogDemo,
|
||||
DropdownMenuCheckboxes,
|
||||
@@ -142,14 +157,17 @@ export const examples = {
|
||||
InputWithButton,
|
||||
InputWithLabel,
|
||||
InputWithText,
|
||||
InputReactHookForm,
|
||||
LabelDemo,
|
||||
MenubarDemo,
|
||||
NavigationMenuDemo,
|
||||
PopoverDemo,
|
||||
ProgressDemo,
|
||||
RadioGroupDemo,
|
||||
RadioGroupReactHookForm,
|
||||
ScrollAreaDemo,
|
||||
SelectDemo,
|
||||
SelectReactHookForm,
|
||||
SeparatorDemo,
|
||||
SheetDemo,
|
||||
SheetSize,
|
||||
@@ -157,6 +175,7 @@ export const examples = {
|
||||
SkeletonDemo,
|
||||
SliderDemo,
|
||||
SwitchDemo,
|
||||
SwitchReactHookForm,
|
||||
TableDemo,
|
||||
TabsDemo,
|
||||
TextareaDemo,
|
||||
@@ -164,6 +183,7 @@ export const examples = {
|
||||
TextareaWithButton,
|
||||
TextareaWithLabel,
|
||||
TextareaWithText,
|
||||
TextareaReactHookForm,
|
||||
ToastDemo,
|
||||
ToastDestructive,
|
||||
ToastSimple,
|
||||
|
||||
65
apps/www/components/examples/input/react-hook-form.tsx
Normal file
65
apps/www/components/examples/input/react-hook-form.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function InputReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...form.register("username")} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
88
apps/www/components/examples/radio-group/react-hook-form.tsx
Normal file
88
apps/www/components/examples/radio-group/react-hook-form.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
"use client"
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
type: z.enum(["all", "mentions", "none"], {
|
||||
required_error: "You need to select a notification type.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function RadioGroupReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="type"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<FormLabel>Notify me about...</FormLabel>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
className="flex flex-col space-y-1"
|
||||
>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="all" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
All new messages
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="mentions" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">
|
||||
Direct messages and mentions
|
||||
</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-3 space-y-0">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="none" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">Nothing</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
84
apps/www/components/examples/select/react-hook-form.tsx
Normal file
84
apps/www/components/examples/select/react-hook-form.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
email: z
|
||||
.string({
|
||||
required_error: "Please select an email to display.",
|
||||
})
|
||||
.email(),
|
||||
})
|
||||
|
||||
export function SelectReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Email</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a verified email to display" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="m@example.com">m@example.com</SelectItem>
|
||||
<SelectItem value="m@google.com">m@google.com</SelectItem>
|
||||
<SelectItem value="m@support.com">m@support.com</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
You can manage email addresses in your{" "}
|
||||
<Link href="/examples/forms">email settings</Link>.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
102
apps/www/components/examples/switch/react-hook-form.tsx
Normal file
102
apps/www/components/examples/switch/react-hook-form.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
marketing_emails: z.boolean().default(false).optional(),
|
||||
security_emails: z.boolean(),
|
||||
})
|
||||
|
||||
export function SwitchReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
security_emails: true,
|
||||
},
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-full space-y-6">
|
||||
<div>
|
||||
<h3 className="mb-4 text-lg font-medium">Email Notifications</h3>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="marketing_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">
|
||||
Marketing emails
|
||||
</FormLabel>
|
||||
<FormDescription>
|
||||
Receive emails about new products, features, and more.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="security_emails"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="text-base">Security emails</FormLabel>
|
||||
<FormDescription>
|
||||
Receive emails about your account security.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
disabled
|
||||
aria-readonly
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
78
apps/www/components/examples/textarea/react-hook-form.tsx
Normal file
78
apps/www/components/examples/textarea/react-hook-form.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { useForm } from "react-hook-form"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Checkbox } from "@/components/ui/checkbox"
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { toast } from "@/components/ui/use-toast"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/react-hook-form/form"
|
||||
|
||||
const FormSchema = z.object({
|
||||
bio: z
|
||||
.string()
|
||||
.min(10, {
|
||||
message: "Bio must be at least 10 characters.",
|
||||
})
|
||||
.max(160, {
|
||||
message: "Bio must not be longer than 30 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function TextareaReactHookForm() {
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
})
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
toast({
|
||||
title: "You submitted the following values:",
|
||||
description: (
|
||||
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
|
||||
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
|
||||
</pre>
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="w-2/3 space-y-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bio"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Bio</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tell us a little bit about yourself"
|
||||
className="resize-none"
|
||||
{...form.register("bio")}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
You can <span>@mention</span> other users and organizations.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@@ -216,7 +216,7 @@ const components = {
|
||||
),
|
||||
Steps: ({ ...props }) => (
|
||||
<div
|
||||
className="[&>h3]:step mb-12 ml-4 border-l pl-6 [counter-reset:step]"
|
||||
className="[&>h3]:step mb-12 ml-4 border-l pl-8 [counter-reset:step]"
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -27,7 +27,7 @@ function PageHeaderHeading({
|
||||
return (
|
||||
<h1
|
||||
className={cn(
|
||||
"text-3xl font-bold leading-tight tracking-tighter md:text-5xl lg:text-6xl lg:leading-[1.1]",
|
||||
"text-3xl font-bold leading-tight tracking-tighter md:text-5xl lg:leading-[1.1]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
179
apps/www/components/react-hook-form/form.tsx
Normal file
179
apps/www/components/react-hook-form/form.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
FieldError,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
FormProvider,
|
||||
get,
|
||||
useController,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
type FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
> = {
|
||||
name: TName
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue
|
||||
)
|
||||
|
||||
const FormField = <
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
|
||||
>({
|
||||
...props
|
||||
}: ControllerProps<TFieldValues, TName>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext)
|
||||
const itemContext = React.useContext(FormItemContext)
|
||||
const controller = useController(fieldContext)
|
||||
|
||||
if (!fieldContext) {
|
||||
throw new Error("useFormField should be used within <FormField>")
|
||||
}
|
||||
|
||||
const { id } = itemContext
|
||||
const error: FieldError =
|
||||
get(controller.fieldState.error, "value") || controller.fieldState.error
|
||||
|
||||
return {
|
||||
id,
|
||||
name: fieldContext.name,
|
||||
formItemId: `${id}-form-item`,
|
||||
formDescriptionId: `${id}-form-item-description`,
|
||||
formMessageId: `${id}-form-item-message`,
|
||||
error,
|
||||
controller,
|
||||
}
|
||||
}
|
||||
|
||||
type FormItemContextValue = {
|
||||
id: string
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue
|
||||
)
|
||||
|
||||
const FormItem = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const id = React.useId()
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ id }}>
|
||||
<div ref={ref} className={cn("space-y-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
)
|
||||
})
|
||||
FormItem.displayName = "FormItem"
|
||||
|
||||
const FormLabel = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { error, formItemId } = useFormField()
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn(error && "text-destructive", className)}
|
||||
htmlFor={formItemId}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormLabel.displayName = "FormLabel"
|
||||
|
||||
const FormControl = React.forwardRef<
|
||||
React.ElementRef<typeof Slot>,
|
||||
React.ComponentPropsWithoutRef<typeof Slot>
|
||||
>(({ ...props }, ref) => {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
|
||||
|
||||
return (
|
||||
<Slot
|
||||
ref={ref}
|
||||
id={formItemId}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionId}`
|
||||
: `${formDescriptionId} ${formMessageId}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormControl.displayName = "FormControl"
|
||||
|
||||
const FormDescription = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { formDescriptionId } = useFormField()
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
})
|
||||
FormDescription.displayName = "FormDescription"
|
||||
|
||||
const FormMessage = React.forwardRef<
|
||||
HTMLParagraphElement,
|
||||
React.HTMLAttributes<HTMLParagraphElement>
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { error, formMessageId } = useFormField()
|
||||
const body = error ? String(error?.message) : children
|
||||
|
||||
if (!body) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
</p>
|
||||
)
|
||||
})
|
||||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function DocsSidebarNavItems({
|
||||
return items?.length ? (
|
||||
<div className="grid grid-flow-row auto-rows-max text-sm">
|
||||
{items.map((item, index) =>
|
||||
item.href ? (
|
||||
item.href && !item.disabled ? (
|
||||
<Link
|
||||
key={index}
|
||||
href={item.href}
|
||||
@@ -65,9 +65,17 @@ export function DocsSidebarNavItems({
|
||||
) : (
|
||||
<span
|
||||
key={index}
|
||||
className="flex w-full cursor-not-allowed items-center rounded-md p-2 text-muted-foreground hover:underline"
|
||||
className={cn(
|
||||
"flex w-full cursor-not-allowed items-center rounded-md p-2 text-muted-foreground hover:underline",
|
||||
item.disabled && "cursor-not-allowed opacity-60"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
{item.label && (
|
||||
<span className="ml-2 rounded-md bg-muted px-1.5 py-0.5 text-xs leading-none text-muted-foreground no-underline group-hover:no-underline">
|
||||
{item.label}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { ModeToggle } from "@/components/mode-toggle"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="supports-backdrop-blur:bg-background/60 sticky top-0 z-40 w-full border-b bg-background/95 shadow-sm backdrop-blur">
|
||||
<header className="supports-backdrop-blur:bg-background/60 sticky top-0 z-40 w-full border-b bg-background/95 backdrop-blur">
|
||||
<div className="container flex h-14 items-center">
|
||||
<MainNav />
|
||||
<MobileNav />
|
||||
|
||||
@@ -75,6 +75,24 @@ export const docsConfig: DocsConfig = {
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Forms",
|
||||
items: [
|
||||
{
|
||||
title: "React Hook Form",
|
||||
href: "/docs/forms/react-hook-form",
|
||||
label: "New",
|
||||
items: [],
|
||||
},
|
||||
{
|
||||
title: "TanStack Form",
|
||||
href: "#",
|
||||
items: [],
|
||||
label: "Soon",
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Components",
|
||||
items: [
|
||||
|
||||
@@ -67,3 +67,16 @@ See the [React DayPicker](https://react-day-picker.js.org) documentation for mor
|
||||
## Date Picker
|
||||
|
||||
You can use the `<Calendar>` component to build a date picker. See the [Date Picker](/docs/components/date-picker) page for more information.
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/calendar/react-hook-form.tsx">
|
||||
<CalendarReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -65,3 +65,24 @@ import { Checkbox } from "@/components/ui/checkbox"
|
||||
<ComponentExample src="/components/examples/checkbox/disabled.tsx">
|
||||
<CheckboxDisabled />
|
||||
</ComponentExample>
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
### Checkbox
|
||||
|
||||
<ComponentExample src="/components/examples/checkbox/react-hook-form-single.tsx">
|
||||
<CheckboxReactHookFormSingle />
|
||||
</ComponentExample>
|
||||
|
||||
### Checkboxes
|
||||
|
||||
<ComponentExample src="/components/examples/checkbox/react-hook-form-multiple.tsx">
|
||||
<CheckboxReactHookFormMultiple />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -130,3 +130,16 @@ export function ComboboxDemo() {
|
||||
<ComponentExample src="/components/examples/combobox/dropdown-menu.tsx">
|
||||
<ComboboxDropdownMenu />
|
||||
</ComponentExample>
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/combobox/react-hook-form.tsx">
|
||||
<ComboboxReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -85,3 +85,16 @@ See the [React DayPicker](https://react-day-picker.js.org) documentation for mor
|
||||
<ComponentExample src="/components/examples/date-picker/with-presets.tsx">
|
||||
<DatePickerWithPresets />
|
||||
</ComponentExample>
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/date-picker/react-hook-form.tsx">
|
||||
<DatePickerReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -102,3 +102,16 @@ import { Input } from "@/components/ui/input"
|
||||
<ComponentExample src="/components/examples/input/file.tsx">
|
||||
<InputFile />
|
||||
</ComponentExample>
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/input/react-hook-form.tsx">
|
||||
<InputReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -61,3 +61,16 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||
</div>
|
||||
</RadioGroup>
|
||||
```
|
||||
|
||||
## React Hook form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/radio-group/react-hook-form.tsx">
|
||||
<RadioGroupReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -68,3 +68,16 @@ import {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
```
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/select/react-hook-form.tsx">
|
||||
<SelectReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -51,3 +51,16 @@ import { Switch } from "@/components/ui/switch"
|
||||
```tsx
|
||||
<Switch />
|
||||
```
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/switch/react-hook-form.tsx">
|
||||
<SwitchReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
@@ -85,3 +85,16 @@ import { Textarea } from "@/components/ui/textarea"
|
||||
<ComponentExample src="/components/examples/textarea/with-button.tsx">
|
||||
<TextareaWithButton />
|
||||
</ComponentExample>
|
||||
|
||||
## React Hook Form
|
||||
|
||||
<Alert className="mt-4">
|
||||
<AlertDescription>
|
||||
**Note**: Learn more about using React Hook Form on the
|
||||
[docs](/docs/forms/react-hook-form) page.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<ComponentExample src="/components/examples/textarea/react-hook-form.tsx">
|
||||
<TextareaReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
224
apps/www/content/docs/forms/react-hook-form.mdx
Normal file
224
apps/www/content/docs/forms/react-hook-form.mdx
Normal file
@@ -0,0 +1,224 @@
|
||||
---
|
||||
title: React Hook Form
|
||||
description: Building forms with React Hook Form 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 [`react-hook-form`](https://react-hook-form.com/) and [`zod`](https://zod.dev). We're going to use a `<FormField>` component to compose accessible forms using Radix UI components.
|
||||
|
||||
## Features
|
||||
|
||||
The `<Form />` component is a wrapper around the `react-hook-form` library. It provides a few things:
|
||||
|
||||
- Composable components for building forms.
|
||||
- A `<FormField />` component for building controlled form fields.
|
||||
- Form validation using `zod`.
|
||||
- Handles accessibility and error messages.
|
||||
- Uses `React.useId()` for generating unique IDs.
|
||||
- Applies the correct `aria` attributes to form fields based on states.
|
||||
- Built to work with all Radix UI components.
|
||||
- Bring your own schema library. We use `zod` but you can use anything you want.
|
||||
- **You have full control over the markup and styling.**
|
||||
|
||||
## Anatomy
|
||||
|
||||
```tsx
|
||||
<Form>
|
||||
<FormField
|
||||
control={...}
|
||||
name="..."
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel />
|
||||
<FormControl>
|
||||
{ /* Your form field */}
|
||||
</FormControl>
|
||||
<FormDescription />
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</Form>
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```tsx
|
||||
const form = useForm()
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...form.register("username")} />
|
||||
</FormControl>
|
||||
<FormDescription>This is your public display name.</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
1. Install the following dependencies:
|
||||
|
||||
```sh
|
||||
npm install react-hook-form zod @hookform/resolvers
|
||||
```
|
||||
|
||||
2. Copy and paste the following `<Form />` component to your app.
|
||||
|
||||
<ComponentSource src="/components/react-hook-form/form.tsx" />
|
||||
|
||||
<Callout>
|
||||
**Note**: You can place this component in a file at `components/ui/form.tsx`
|
||||
and import it from there.
|
||||
</Callout>
|
||||
|
||||
## 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).
|
||||
|
||||
```tsx showLineNumbers {4,6-8}
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2).max(50),
|
||||
})
|
||||
```
|
||||
|
||||
### Define a form
|
||||
|
||||
Use the `useForm` hook from `react-hook-form` to create a form.
|
||||
|
||||
```tsx showLineNumbers {4,14-17,19-24}
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import * as z from "zod"
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function ProfileForm() {
|
||||
// 1. Define your form.
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
})
|
||||
|
||||
// 2. Define a submit handler.
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
// Do something with the form values.
|
||||
// ✅ This will be type-safe and validated.
|
||||
console.log(values)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Build your form
|
||||
|
||||
We can now use the `<Form />` components to build our form.
|
||||
|
||||
```tsx showLineNumbers {7-17,28-50}
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import * as z from "zod"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/form"
|
||||
|
||||
const formSchema = z.object({
|
||||
username: z.string().min(2, {
|
||||
message: "Username must be at least 2 characters.",
|
||||
}),
|
||||
})
|
||||
|
||||
export function ProfileForm() {
|
||||
// ...
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="shadcn" {...form.register("username")} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your public display name.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Submit</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Done
|
||||
|
||||
That's it. You now have a fully accessible form that is type-safe with client-side validation.
|
||||
|
||||
<ComponentExample
|
||||
src="/components/examples/input/react-hook-form.tsx"
|
||||
className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden"
|
||||
>
|
||||
<InputReactHookForm />
|
||||
</ComponentExample>
|
||||
|
||||
</Steps>
|
||||
|
||||
## Examples
|
||||
|
||||
See the following links for more examples on how to use the `<Form />` component with other components:
|
||||
|
||||
- [Checkbox](/docs/components/checkbox#react-hook-form)
|
||||
- [Date Picker](/docs/components/date-picker#react-hook-form)
|
||||
- [Input](/docs/components/input#react-hook-form)
|
||||
- [Radio Group](/docs/components/radio-group#react-hook-form)
|
||||
- [Select](/docs/components/select#react-hook-form)
|
||||
- [Switch](/docs/components/switch#react-hook-form)
|
||||
- [Textarea](/docs/components/textarea#react-hook-form)
|
||||
- [Combobox](/docs/components/combobox#react-hook-form)
|
||||
@@ -37,6 +37,11 @@ const nextConfig = {
|
||||
destination: "/docs/figma",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/docs/forms",
|
||||
destination: "/docs/forms/react-hook-form",
|
||||
permanent: false,
|
||||
},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^7.6.0",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@radix-ui/react-accessible-icon": "^1.0.1",
|
||||
"@radix-ui/react-accordion": "^1.1.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.0.2",
|
||||
@@ -60,6 +61,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.6.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-wrap-balancer": "^0.4.0",
|
||||
"recharts": "^2.5.0",
|
||||
"sharp": "^0.31.3",
|
||||
|
||||
BIN
apps/www/public/examples/forms-dark.png
Normal file
BIN
apps/www/public/examples/forms-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
apps/www/public/examples/forms-light.png
Normal file
BIN
apps/www/public/examples/forms-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
99
pnpm-lock.yaml
generated
99
pnpm-lock.yaml
generated
@@ -97,6 +97,9 @@ importers:
|
||||
'@faker-js/faker':
|
||||
specifier: ^7.6.0
|
||||
version: 7.6.0
|
||||
'@hookform/resolvers':
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(react-hook-form@7.43.9)
|
||||
'@radix-ui/react-accessible-icon':
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(react-dom@18.2.0)(react@18.2.0)
|
||||
@@ -226,6 +229,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^18.2.0
|
||||
version: 18.2.0(react@18.2.0)
|
||||
react-hook-form:
|
||||
specifier: ^7.43.9
|
||||
version: 7.43.9(react@18.2.0)
|
||||
react-wrap-balancer:
|
||||
specifier: ^0.4.0
|
||||
version: 0.4.0(react@18.2.0)
|
||||
@@ -1574,10 +1580,12 @@ packages:
|
||||
dependencies:
|
||||
eslint: 8.38.0
|
||||
eslint-visitor-keys: 3.4.0
|
||||
dev: false
|
||||
|
||||
/@eslint-community/regexpp@4.5.0:
|
||||
resolution: {integrity: sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==}
|
||||
engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/@eslint/eslintrc@1.4.1:
|
||||
resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
|
||||
@@ -1594,7 +1602,6 @@ packages:
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@eslint/eslintrc@2.0.2:
|
||||
resolution: {integrity: sha512-3W4f5tDUra+pA+FzgugqL2pRimUTDJWKr7BINqOpkZrC0uYI0NIc0/JFgBROCU07HR6GieA5m3/rsPIhDmCXTQ==}
|
||||
@@ -1611,10 +1618,12 @@ packages:
|
||||
strip-json-comments: 3.1.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@eslint/js@8.38.0:
|
||||
resolution: {integrity: sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==}
|
||||
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
|
||||
dev: false
|
||||
|
||||
/@faker-js/faker@7.6.0:
|
||||
resolution: {integrity: sha512-XK6BTq1NDMo9Xqw/YkYyGjSsg44fbNwYRx7QK2CuoQgyy+f1rrTDHoExVM5PsyXCtfl2vs2vVJ0MN0yN6LppRw==}
|
||||
@@ -1681,6 +1690,14 @@ packages:
|
||||
yargs: 16.2.0
|
||||
dev: false
|
||||
|
||||
/@hookform/resolvers@3.1.0(react-hook-form@7.43.9):
|
||||
resolution: {integrity: sha512-z0A8K+Nxq+f83Whm/ajlwE6VtQlp/yPHZnXw7XWVPIGm1Vx0QV8KThU3BpbBRfAZ7/dYqCKKBNnQh85BkmBKkA==}
|
||||
peerDependencies:
|
||||
react-hook-form: ^7.0.0
|
||||
dependencies:
|
||||
react-hook-form: 7.43.9(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.11.8:
|
||||
resolution: {integrity: sha512-UybHIJzJnR5Qc/MsD9Kr+RpO2h+/P1GhOwdiLPXK5TWk5sgTdu88bTD9UP+CKbPPh5Rni1u0GjAdYQLemG8g+g==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
@@ -2265,6 +2282,7 @@ packages:
|
||||
picocolors: 1.0.0
|
||||
tiny-glob: 0.2.9
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/@protobufjs/aspromise@1.1.2:
|
||||
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
|
||||
@@ -3624,7 +3642,6 @@ packages:
|
||||
typescript: 4.9.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/@typescript-eslint/parser@5.58.0(eslint@8.38.0)(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-ixaM3gRtlfrKzP8N6lRhBbjTow1t6ztfBvQNGuRM8qH1bjFFXIJ35XY+FC0RRBKn3C6cT+7VW1y8tNm7DwPHDQ==}
|
||||
@@ -3644,6 +3661,7 @@ packages:
|
||||
typescript: 4.9.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/@typescript-eslint/scope-manager@5.58.0:
|
||||
resolution: {integrity: sha512-b+w8ypN5CFvrXWQb9Ow9T4/6LC2MikNf1viLkYTiTbkQl46CnR69w7lajz1icW0TBsYmlpg+mRzFJ4LEJ8X9NA==}
|
||||
@@ -4679,6 +4697,7 @@ packages:
|
||||
/define-lazy-prop@2.0.0:
|
||||
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
|
||||
engines: {node: '>=8'}
|
||||
dev: false
|
||||
|
||||
/define-properties@1.2.0:
|
||||
resolution: {integrity: sha512-xvqAVKGfT1+UAvPwKTVw/njhdQ8ZhXK4lI0bCIuCMrp2up9nPnaDftrLtmpTazqd1o+UY4zgzU+avtMbDP+ldA==}
|
||||
@@ -4785,6 +4804,7 @@ packages:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.2.1
|
||||
dev: false
|
||||
|
||||
/enquirer@2.3.6:
|
||||
resolution: {integrity: sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==}
|
||||
@@ -4963,7 +4983,7 @@ packages:
|
||||
eslint: 8.31.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5)(eslint@8.31.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0)
|
||||
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.31.0)
|
||||
eslint-plugin-react: 7.32.2(eslint@8.31.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.31.0)
|
||||
@@ -4988,7 +5008,7 @@ packages:
|
||||
eslint: 8.38.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.38.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0)
|
||||
eslint-plugin-jsx-a11y: 6.7.1(eslint@8.38.0)
|
||||
eslint-plugin-react: 7.32.2(eslint@8.38.0)
|
||||
eslint-plugin-react-hooks: 4.6.0(eslint@8.38.0)
|
||||
@@ -5043,14 +5063,13 @@ packages:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
eslint: 8.31.0
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0)
|
||||
glob: 7.2.3
|
||||
is-glob: 4.0.3
|
||||
resolve: 1.22.2
|
||||
tsconfig-paths: 3.14.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint-import-resolver-typescript@3.5.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.38.0):
|
||||
resolution: {integrity: sha512-TdJqPHs2lW5J9Zpe17DZNQuDnox4xo2o+0tE7Pggain9Rbc19ik8kFtXdxZ250FVx2kF4vlt2RSf4qlUpG7bhw==}
|
||||
@@ -5063,7 +5082,7 @@ packages:
|
||||
enhanced-resolve: 5.12.0
|
||||
eslint: 8.38.0
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0)
|
||||
eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0)
|
||||
get-tsconfig: 4.5.0
|
||||
globby: 13.1.4
|
||||
is-core-module: 2.12.0
|
||||
@@ -5074,6 +5093,36 @@ packages:
|
||||
- eslint-import-resolver-node
|
||||
- eslint-import-resolver-webpack
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/eslint-module-utils@2.8.0(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0):
|
||||
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/parser': '*'
|
||||
eslint: '*'
|
||||
eslint-import-resolver-node: '*'
|
||||
eslint-import-resolver-typescript: '*'
|
||||
eslint-import-resolver-webpack: '*'
|
||||
peerDependenciesMeta:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
eslint:
|
||||
optional: true
|
||||
eslint-import-resolver-node:
|
||||
optional: true
|
||||
eslint-import-resolver-typescript:
|
||||
optional: true
|
||||
eslint-import-resolver-webpack:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.58.0(eslint@8.31.0)(typescript@4.9.5)
|
||||
debug: 3.2.7
|
||||
eslint: 8.31.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.27.5)(eslint@8.31.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
/eslint-module-utils@2.8.0(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0):
|
||||
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
|
||||
@@ -5103,8 +5152,9 @@ packages:
|
||||
eslint-import-resolver-typescript: 3.5.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-plugin-import@2.27.5)(eslint@8.38.0)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0):
|
||||
/eslint-plugin-import@2.27.5(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0):
|
||||
resolution: {integrity: sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==}
|
||||
engines: {node: '>=4'}
|
||||
peerDependencies:
|
||||
@@ -5114,15 +5164,15 @@ packages:
|
||||
'@typescript-eslint/parser':
|
||||
optional: true
|
||||
dependencies:
|
||||
'@typescript-eslint/parser': 5.58.0(eslint@8.38.0)(typescript@4.9.5)
|
||||
'@typescript-eslint/parser': 5.58.0(eslint@8.31.0)(typescript@4.9.5)
|
||||
array-includes: 3.1.6
|
||||
array.prototype.flat: 1.3.1
|
||||
array.prototype.flatmap: 1.3.1
|
||||
debug: 3.2.7
|
||||
doctrine: 2.1.0
|
||||
eslint: 8.38.0
|
||||
eslint: 8.31.0
|
||||
eslint-import-resolver-node: 0.3.7
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@3.5.5)(eslint@8.38.0)
|
||||
eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.58.0)(eslint-import-resolver-node@0.3.7)(eslint-import-resolver-typescript@2.7.1)(eslint@8.31.0)
|
||||
has: 1.0.3
|
||||
is-core-module: 2.12.0
|
||||
is-glob: 4.0.3
|
||||
@@ -5285,12 +5335,10 @@ packages:
|
||||
dependencies:
|
||||
eslint: 8.31.0
|
||||
eslint-visitor-keys: 2.1.0
|
||||
dev: true
|
||||
|
||||
/eslint-visitor-keys@2.1.0:
|
||||
resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/eslint-visitor-keys@3.4.0:
|
||||
resolution: {integrity: sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==}
|
||||
@@ -5342,7 +5390,6 @@ packages:
|
||||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/eslint@8.38.0:
|
||||
resolution: {integrity: sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==}
|
||||
@@ -5391,6 +5438,7 @@ packages:
|
||||
text-table: 0.2.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
/espree@9.5.1:
|
||||
resolution: {integrity: sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==}
|
||||
@@ -5766,6 +5814,7 @@ packages:
|
||||
|
||||
/get-tsconfig@4.5.0:
|
||||
resolution: {integrity: sha512-MjhiaIWCJ1sAU4pIQ5i5OfOuHHxVo1oYeNsWTON7jxYkod8pHocXeh+SSbmu5OZZZK73B6cbJ2XADzXehLyovQ==}
|
||||
dev: false
|
||||
|
||||
/git-raw-commits@2.0.11:
|
||||
resolution: {integrity: sha512-VnctFhw+xfj8Va1xtfEqCUD2XDrbAPSJx+hSrE5K7fGdjZruW7XV+QOrN7LF/RJyvspRiD2I0asWsxFp0ya26A==}
|
||||
@@ -5858,6 +5907,7 @@ packages:
|
||||
|
||||
/globalyzer@0.1.0:
|
||||
resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==}
|
||||
dev: false
|
||||
|
||||
/globby@11.1.0:
|
||||
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
|
||||
@@ -5879,9 +5929,11 @@ packages:
|
||||
ignore: 5.2.4
|
||||
merge2: 1.4.1
|
||||
slash: 4.0.0
|
||||
dev: false
|
||||
|
||||
/globrex@0.1.2:
|
||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||
dev: false
|
||||
|
||||
/gopd@1.0.1:
|
||||
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
|
||||
@@ -5909,6 +5961,7 @@ packages:
|
||||
|
||||
/graceful-fs@4.2.11:
|
||||
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
|
||||
dev: false
|
||||
|
||||
/grapheme-splitter@1.0.4:
|
||||
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
|
||||
@@ -6299,6 +6352,7 @@ packages:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/is-extendable@0.1.1:
|
||||
resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==}
|
||||
@@ -6469,6 +6523,7 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
dev: false
|
||||
|
||||
/isarray@2.0.5:
|
||||
resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
|
||||
@@ -7800,6 +7855,7 @@ packages:
|
||||
define-lazy-prop: 2.0.0
|
||||
is-docker: 2.2.1
|
||||
is-wsl: 2.2.0
|
||||
dev: false
|
||||
|
||||
/optionator@0.9.1:
|
||||
resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==}
|
||||
@@ -8264,6 +8320,15 @@ packages:
|
||||
scheduler: 0.23.0
|
||||
dev: false
|
||||
|
||||
/react-hook-form@7.43.9(react@18.2.0):
|
||||
resolution: {integrity: sha512-AUDN3Pz2NSeoxQ7Hs6OhQhDr6gtF9YRuutGDwPQqhSUAHJSgGl2VeY3qN19MG0SucpjgDiuMJ4iC5T5uB+eaNQ==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17 || ^18
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/react-is@16.13.1:
|
||||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
|
||||
|
||||
@@ -8502,7 +8567,6 @@ packages:
|
||||
/regexpp@3.2.0:
|
||||
resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==}
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/registry-auth-token@4.2.2:
|
||||
resolution: {integrity: sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==}
|
||||
@@ -8935,6 +8999,7 @@ packages:
|
||||
/slash@4.0.0:
|
||||
resolution: {integrity: sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/smartwrap@2.0.2:
|
||||
resolution: {integrity: sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA==}
|
||||
@@ -9226,6 +9291,7 @@ packages:
|
||||
dependencies:
|
||||
'@pkgr/utils': 2.3.1
|
||||
tslib: 2.5.0
|
||||
dev: false
|
||||
|
||||
/tailwind-merge@1.12.0:
|
||||
resolution: {integrity: sha512-Y17eDp7FtN1+JJ4OY0Bqv9OA41O+MS8c1Iyr3T6JFLnOgLg3EvcyMKZAnQ8AGyvB5Nxm3t9Xb5Mhe139m8QT/g==}
|
||||
@@ -9276,6 +9342,7 @@ packages:
|
||||
/tapable@2.2.1:
|
||||
resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/tar-fs@2.1.1:
|
||||
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
|
||||
@@ -9336,6 +9403,7 @@ packages:
|
||||
dependencies:
|
||||
globalyzer: 0.1.0
|
||||
globrex: 0.1.2
|
||||
dev: false
|
||||
|
||||
/tmp@0.0.33:
|
||||
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||
@@ -9452,6 +9520,7 @@ packages:
|
||||
|
||||
/tslib@2.5.0:
|
||||
resolution: {integrity: sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==}
|
||||
dev: false
|
||||
|
||||
/tsup@6.6.3(postcss@8.4.21)(ts-node@10.9.1)(typescript@4.9.5):
|
||||
resolution: {integrity: sha512-OLx/jFllYlVeZQ7sCHBuRVEQBBa1tFbouoc/gbYakyipjVQdWy/iQOvmExUA/ewap9iQ7tbJf9pW0PgcEFfJcQ==}
|
||||
|
||||
Reference in New Issue
Block a user