|
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
|
} |