From 4ccff13f9c68cd12bca289faa8d513b3f748ea37 Mon Sep 17 00:00:00 2001 From: shadcn Date: Fri, 19 May 2023 22:56:49 +0400 Subject: [PATCH] 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 --- .../dashboard/components/team-switcher.tsx | 2 +- .../examples/forms/account/account-form.tsx | 218 +++++++++++++++++ apps/www/app/examples/forms/account/page.tsx | 18 ++ .../forms/appearance/appearance-form.tsx | 164 +++++++++++++ .../app/examples/forms/appearance/page.tsx | 18 ++ .../examples/forms/components/sidebar-nav.tsx | 44 ++++ .../examples/forms/display/display-form.tsx | 132 +++++++++++ apps/www/app/examples/forms/display/page.tsx | 17 ++ apps/www/app/examples/forms/layout.tsx | 75 ++++++ .../notifications/notifications-form.tsx | 222 +++++++++++++++++ .../app/examples/forms/notifications/page.tsx | 18 ++ apps/www/app/examples/forms/page.tsx | 17 ++ apps/www/app/examples/forms/profile-form.tsx | 191 +++++++++++++++ apps/www/app/examples/layout.tsx | 4 +- apps/www/app/page.tsx | 7 +- apps/www/components/examples-nav.tsx | 7 +- .../examples/calendar/react-hook-form.tsx | 101 ++++++++ .../checkbox/react-hook-form-multiple.tsx | 127 ++++++++++ .../checkbox/react-hook-form-single.tsx | 73 ++++++ .../examples/combobox/react-hook-form.tsx | 135 +++++++++++ .../examples/date-picker/react-hook-form.tsx | 101 ++++++++ apps/www/components/examples/index.tsx | 20 ++ .../examples/input/react-hook-form.tsx | 65 +++++ .../examples/radio-group/react-hook-form.tsx | 88 +++++++ .../examples/select/react-hook-form.tsx | 84 +++++++ .../examples/switch/react-hook-form.tsx | 102 ++++++++ .../examples/textarea/react-hook-form.tsx | 78 ++++++ apps/www/components/mdx-components.tsx | 2 +- apps/www/components/page-header.tsx | 2 +- apps/www/components/react-hook-form/form.tsx | 179 ++++++++++++++ apps/www/components/sidebar-nav.tsx | 12 +- apps/www/components/site-header.tsx | 2 +- apps/www/config/docs.ts | 18 ++ apps/www/content/docs/components/calendar.mdx | 13 + apps/www/content/docs/components/checkbox.mdx | 21 ++ apps/www/content/docs/components/combobox.mdx | 13 + .../content/docs/components/date-picker.mdx | 13 + apps/www/content/docs/components/input.mdx | 13 + .../content/docs/components/radio-group.mdx | 13 + apps/www/content/docs/components/select.mdx | 13 + apps/www/content/docs/components/switch.mdx | 13 + apps/www/content/docs/components/textarea.mdx | 13 + .../content/docs/forms/react-hook-form.mdx | 224 ++++++++++++++++++ apps/www/next.config.mjs | 5 + apps/www/package.json | 2 + apps/www/public/examples/forms-dark.png | Bin 0 -> 32876 bytes apps/www/public/examples/forms-light.png | Bin 0 -> 29685 bytes pnpm-lock.yaml | 99 ++++++-- 48 files changed, 2770 insertions(+), 28 deletions(-) create mode 100644 apps/www/app/examples/forms/account/account-form.tsx create mode 100644 apps/www/app/examples/forms/account/page.tsx create mode 100644 apps/www/app/examples/forms/appearance/appearance-form.tsx create mode 100644 apps/www/app/examples/forms/appearance/page.tsx create mode 100644 apps/www/app/examples/forms/components/sidebar-nav.tsx create mode 100644 apps/www/app/examples/forms/display/display-form.tsx create mode 100644 apps/www/app/examples/forms/display/page.tsx create mode 100644 apps/www/app/examples/forms/layout.tsx create mode 100644 apps/www/app/examples/forms/notifications/notifications-form.tsx create mode 100644 apps/www/app/examples/forms/notifications/page.tsx create mode 100644 apps/www/app/examples/forms/page.tsx create mode 100644 apps/www/app/examples/forms/profile-form.tsx create mode 100644 apps/www/components/examples/calendar/react-hook-form.tsx create mode 100644 apps/www/components/examples/checkbox/react-hook-form-multiple.tsx create mode 100644 apps/www/components/examples/checkbox/react-hook-form-single.tsx create mode 100644 apps/www/components/examples/combobox/react-hook-form.tsx create mode 100644 apps/www/components/examples/date-picker/react-hook-form.tsx create mode 100644 apps/www/components/examples/input/react-hook-form.tsx create mode 100644 apps/www/components/examples/radio-group/react-hook-form.tsx create mode 100644 apps/www/components/examples/select/react-hook-form.tsx create mode 100644 apps/www/components/examples/switch/react-hook-form.tsx create mode 100644 apps/www/components/examples/textarea/react-hook-form.tsx create mode 100644 apps/www/components/react-hook-form/form.tsx create mode 100644 apps/www/content/docs/forms/react-hook-form.mdx create mode 100644 apps/www/public/examples/forms-dark.png create mode 100644 apps/www/public/examples/forms-light.png diff --git a/apps/www/app/examples/dashboard/components/team-switcher.tsx b/apps/www/app/examples/dashboard/components/team-switcher.tsx index c4496ee80..2f9b58e0c 100644 --- a/apps/www/app/examples/dashboard/components/team-switcher.tsx +++ b/apps/www/app/examples/dashboard/components/team-switcher.tsx @@ -82,7 +82,7 @@ export default function TeamSwitcher({ className }: TeamSwitcherProps) { + + + + + date > new Date() || date < new Date("1900-01-01") + } + initialFocus + /> + + + + Your date of birth is used to calculate your age. + + + + )} + /> + ( + + Language + + + + + + + + + + No framework found. + + {languages.map((language) => ( + { + form.setValue("language", value) + }} + > + + {language.label} + + ))} + + + + + + This is the language that will be used in the dashboard. + + + + )} + /> + + + + ) +} diff --git a/apps/www/app/examples/forms/account/page.tsx b/apps/www/app/examples/forms/account/page.tsx new file mode 100644 index 000000000..f48c4e1d3 --- /dev/null +++ b/apps/www/app/examples/forms/account/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AccountForm } from "@/app/examples/forms/account/account-form" + +export default function SettingsAccountPage() { + return ( +
+
+

Account

+

+ Update your account settings. Set your preferred language and + timezone. +

+
+ + +
+ ) +} diff --git a/apps/www/app/examples/forms/appearance/appearance-form.tsx b/apps/www/app/examples/forms/appearance/appearance-form.tsx new file mode 100644 index 000000000..21229028f --- /dev/null +++ b/apps/www/app/examples/forms/appearance/appearance-form.tsx @@ -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 + +// This can come from your database or API. +const defaultValues: Partial = { + theme: "light", +} + +export function AppearanceForm() { + const form = useForm({ + resolver: zodResolver(appearanceFormSchema), + defaultValues, + }) + + function onSubmit(data: AppearanceFormValues) { + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }) + } + + return ( +
+ + ( + + Font +
+ + + + +
+ + Set the font you want to use in the dashboard. + + +
+ )} + /> + ( + + Theme + + Select the theme for the dashboard. + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Light + + + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + Dark + + + + + + )} + /> + + + + + ) +} diff --git a/apps/www/app/examples/forms/appearance/page.tsx b/apps/www/app/examples/forms/appearance/page.tsx new file mode 100644 index 000000000..929253454 --- /dev/null +++ b/apps/www/app/examples/forms/appearance/page.tsx @@ -0,0 +1,18 @@ +import { Separator } from "@/components/ui/separator" +import { AppearanceForm } from "@/app/examples/forms/appearance/appearance-form" + +export default function SettingsAppearancePage() { + return ( +
+
+

Appearance

+

+ Customize the appearance of the app. Automatically switch between day + and night themes. +

+
+ + +
+ ) +} diff --git a/apps/www/app/examples/forms/components/sidebar-nav.tsx b/apps/www/app/examples/forms/components/sidebar-nav.tsx new file mode 100644 index 000000000..addcfefdc --- /dev/null +++ b/apps/www/app/examples/forms/components/sidebar-nav.tsx @@ -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 { + items: { + href: string + title: string + }[] +} + +export function SidebarNav({ className, items, ...props }: SidebarNavProps) { + const pathname = usePathname() + + return ( + + ) +} diff --git a/apps/www/app/examples/forms/display/display-form.tsx b/apps/www/app/examples/forms/display/display-form.tsx new file mode 100644 index 000000000..09147907a --- /dev/null +++ b/apps/www/app/examples/forms/display/display-form.tsx @@ -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 + +// This can come from your database or API. +const defaultValues: Partial = { + items: ["recents", "home"], +} + +export function DisplayForm() { + const form = useForm({ + resolver: zodResolver(displayFormSchema), + defaultValues, + }) + + function onSubmit(data: DisplayFormValues) { + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }) + } + + return ( +
+ + ( + +
+ Sidebar + + Select the items you want to display in the sidebar. + +
+ {items.map((item) => ( + { + return ( + + + { + return checked + ? field.onChange([...field.value, item.id]) + : field.onChange( + field.value?.filter( + (value) => value !== item.id + ) + ) + }} + /> + + + {item.label} + + + ) + }} + /> + ))} + +
+ )} + /> + + + + ) +} diff --git a/apps/www/app/examples/forms/display/page.tsx b/apps/www/app/examples/forms/display/page.tsx new file mode 100644 index 000000000..5d8942301 --- /dev/null +++ b/apps/www/app/examples/forms/display/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { DisplayForm } from "@/app/examples/forms/display/display-form" + +export default function SettingsDisplayPage() { + return ( +
+
+

Display

+

+ Turn items on or off to control what's displayed in the app. +

+
+ + +
+ ) +} diff --git a/apps/www/app/examples/forms/layout.tsx b/apps/www/app/examples/forms/layout.tsx new file mode 100644 index 000000000..36208d26b --- /dev/null +++ b/apps/www/app/examples/forms/layout.tsx @@ -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 ( + <> +
+ Forms + Forms +
+
+
+

Settings

+

+ Manage your account settings and set e-mail preferences. +

+
+ +
+ +
{children}
+
+
+ + ) +} diff --git a/apps/www/app/examples/forms/notifications/notifications-form.tsx b/apps/www/app/examples/forms/notifications/notifications-form.tsx new file mode 100644 index 000000000..344719cf6 --- /dev/null +++ b/apps/www/app/examples/forms/notifications/notifications-form.tsx @@ -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 + +// This can come from your database or API. +const defaultValues: Partial = { + communication_emails: false, + marketing_emails: false, + social_emails: true, + security_emails: true, +} + +export function NotificationsForm() { + const form = useForm({ + resolver: zodResolver(notificationsFormSchema), + defaultValues, + }) + + function onSubmit(data: NotificationsFormValues) { + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }) + } + + return ( +
+ + ( + + Notify me about... + + + + + + + + All new messages + + + + + + + + Direct messages and mentions + + + + + + + Nothing + + + + + + )} + /> +
+

Email Notifications

+
+ ( + +
+ + Communication emails + + + Receive emails about your account activity. + +
+ + + +
+ )} + /> + ( + +
+ + Marketing emails + + + Receive emails about new products, features, and more. + +
+ + + +
+ )} + /> + ( + +
+ Social emails + + Receive emails for friend requests, follows, and more. + +
+ + + +
+ )} + /> + ( + +
+ Security emails + + Receive emails about your account activity and security. + +
+ + + +
+ )} + /> +
+
+ ( + + + + +
+ + Use different settings for my mobile devices + + + You can manage your mobile notifications in the{" "} + mobile settings page. + +
+
+ )} + /> + + + + ) +} diff --git a/apps/www/app/examples/forms/notifications/page.tsx b/apps/www/app/examples/forms/notifications/page.tsx new file mode 100644 index 000000000..659dd4b0a --- /dev/null +++ b/apps/www/app/examples/forms/notifications/page.tsx @@ -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 ( +
+
+

Notifications

+

+ Configure how you receive notifications. +

+
+ + +
+ ) +} diff --git a/apps/www/app/examples/forms/page.tsx b/apps/www/app/examples/forms/page.tsx new file mode 100644 index 000000000..3f5f33664 --- /dev/null +++ b/apps/www/app/examples/forms/page.tsx @@ -0,0 +1,17 @@ +import { Separator } from "@/components/ui/separator" +import { ProfileForm } from "@/app/examples/forms/profile-form" + +export default function SettingsProfilePage() { + return ( +
+
+

Profile

+

+ This is how others will see you on the site. +

+
+ + +
+ ) +} diff --git a/apps/www/app/examples/forms/profile-form.tsx b/apps/www/app/examples/forms/profile-form.tsx new file mode 100644 index 000000000..ec94aa525 --- /dev/null +++ b/apps/www/app/examples/forms/profile-form.tsx @@ -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 + +// This can come from your database or API. +const defaultValues: Partial = { + bio: "I own a computer.", + urls: [ + { value: "https://shadcn.com" }, + { value: "http://twitter.com/shadcn" }, + ], +} + +export function ProfileForm() { + const form = useForm({ + 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: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }) + } + + return ( +
+ + ( + + Username + + + + + This is your public display name. It can be your real name or a + pseudonym. You can only change this once every 30 days. + + + + )} + /> + ( + + Email + + + You can manage verified email addresses in your{" "} + email settings. + + + + )} + /> + ( + + Bio + +