1 "use client"
2
3 import * as React from "react"
4 import * as LabelPrimitive from "@radix-ui/react-label"
5 import { Slot } from "@radix-ui/react-slot"
6 import {
7 Controller,
8 FormProvider,
9 useFormContext,
10 type ControllerProps,
11 type FieldPath,
12 type FieldValues,
13 } from "react-hook-form"
14
15 import { cn } from "@/lib/utils"
16 import { Label } from "@/components/ui/label"
17
18 const Form = FormProvider
19
20 type FormFieldContextValue<
21 TFieldValues extends FieldValues = FieldValues,
22 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
23 > = {
24 name: TName
25 }
26
27 const FormFieldContext = React.createContext<FormFieldContextValue>(
28 {} as FormFieldContextValue
29 )
30
31 const FormField = <
32 TFieldValues extends FieldValues = FieldValues,
33 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
34 >({
35 ...props
36 }: ControllerProps<TFieldValues, TName>) => {
37 return (
38 <FormFieldContext.Provider value={{ name: props.name }}>
39 <Controller {...props} />
40 </FormFieldContext.Provider>
41 )
42 }
43
44 const useFormField = () => {
45 const fieldContext = React.useContext(FormFieldContext)
46 const itemContext = React.useContext(FormItemContext)
47 const { getFieldState, formState } = useFormContext()
48
49 const fieldState = getFieldState(fieldContext.name, formState)
50
51 if (!fieldContext) {
52 throw new Error("useFormField should be used within <FormField>")
53 }
54
55 const { id } = itemContext
56
57 return {
58 id,
59 name: fieldContext.name,
60 formItemId: `${id}-form-item`,
61 formDescriptionId: `${id}-form-item-description`,
62 formMessageId: `${id}-form-item-message`,
63 ...fieldState,
64 }
65 }
66
67 type FormItemContextValue = {
68 id: string
69 }
70
71 const FormItemContext = React.createContext<FormItemContextValue>(
72 {} as FormItemContextValue
73 )
74
75 const FormItem = React.forwardRef<
76 HTMLDivElement,
77 React.HTMLAttributes<HTMLDivElement>
78 >(({ className, ...props }, ref) => {
79 const id = React.useId()
80
81 return (
82 <FormItemContext.Provider value={{ id }}>
83 <div ref={ref} className={cn("space-y-2", className)} {...props} />
84 </FormItemContext.Provider>
85 )
86 })
87 FormItem.displayName = "FormItem"
88
89 const FormLabel = React.forwardRef<
90 React.ElementRef<typeof LabelPrimitive.Root>,
91 React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
92 >(({ className, ...props }, ref) => {
93 const { error, formItemId } = useFormField()
94
95 return (
96 <Label
97 ref={ref}
98 className={cn(error && "text-destructive", className)}
99 htmlFor={formItemId}
100 {...props}
101 />
102 )
103 })
104 FormLabel.displayName = "FormLabel"
105
106 const FormControl = React.forwardRef<
107 React.ElementRef<typeof Slot>,
108 React.ComponentPropsWithoutRef<typeof Slot>
109 >(({ ...props }, ref) => {
110 const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
111
112 return (
113 <Slot
114 ref={ref}
115 id={formItemId}
116 aria-describedby={
117 !error
118 ? `${formDescriptionId}`
119 : `${formDescriptionId} ${formMessageId}`
120 }
121 aria-invalid={!!error}
122 {...props}
123 />
124 )
125 })
126 FormControl.displayName = "FormControl"
127
128 const FormDescription = React.forwardRef<
129 HTMLParagraphElement,
130 React.HTMLAttributes<HTMLParagraphElement>
131 >(({ className, ...props }, ref) => {
132 const { formDescriptionId } = useFormField()
133
134 return (
135 <p
136 ref={ref}
137 id={formDescriptionId}
138 className={cn("text-sm text-muted-foreground", className)}
139 {...props}
140 />
141 )
142 })
143 FormDescription.displayName = "FormDescription"
144
145 const FormMessage = React.forwardRef<
146 HTMLParagraphElement,
147 React.HTMLAttributes<HTMLParagraphElement>
148 >(({ className, children, ...props }, ref) => {
149 const { error, formMessageId } = useFormField()
150 const body = error ? String(error?.message ?? "") : children
151
152 if (!body) {
153 return null
154 }
155
156 return (
157 <p
158 ref={ref}
159 id={formMessageId}
160 className={cn("text-sm font-medium text-destructive", className)}
161 {...props}
162 >
163 {body}
164 </p>
165 )
166 })
167 FormMessage.displayName = "FormMessage"
168
169 export {
170 useFormField,
171 Form,
172 FormItem,
173 FormLabel,
174 FormControl,
175 FormDescription,
176 FormMessage,
177 FormField,
178 }