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