form.tsx
1 import * as React from "react"; 2 import * as LabelPrimitive from "@radix-ui/react-label"; 3 import { Slot } from "@radix-ui/react-slot"; 4 import { 5 Controller, 6 ControllerProps, 7 FieldPath, 8 FieldValues, 9 FormProvider, 10 useFormContext, 11 } from "react-hook-form"; 12 13 import { cn } from "../../lib/utils"; 14 import { Label } from "./label"; 15 16 const Form = FormProvider; 17 18 type FormFieldContextValue< 19 TFieldValues extends FieldValues = FieldValues, 20 TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>, 21 > = { 22 name: TName; 23 }; 24 25 const FormFieldContext = React.createContext<FormFieldContextValue>( 26 {} as FormFieldContextValue, 27 ); 28 29 const 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 42 const 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 65 type FormItemContextValue = { 66 id: string; 67 }; 68 69 const FormItemContext = React.createContext<FormItemContextValue>( 70 {} as FormItemContextValue, 71 ); 72 73 const 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 }); 85 FormItem.displayName = "FormItem"; 86 87 const 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-red-500 dark:text-red-900", className)} 97 htmlFor={formItemId} 98 {...props} 99 /> 100 ); 101 }); 102 FormLabel.displayName = "FormLabel"; 103 104 const FormControl = React.forwardRef< 105 React.ElementRef<typeof Slot>, 106 React.ComponentPropsWithoutRef<typeof Slot> 107 >(({ ...props }, ref) => { 108 const { error, formItemId, formDescriptionId, formMessageId } = 109 useFormField(); 110 111 return ( 112 <Slot 113 ref={ref} 114 id={formItemId} 115 aria-describedby={ 116 !error 117 ? `${formDescriptionId}` 118 : `${formDescriptionId} ${formMessageId}` 119 } 120 aria-invalid={!!error} 121 {...props} 122 /> 123 ); 124 }); 125 FormControl.displayName = "FormControl"; 126 127 const FormDescription = React.forwardRef< 128 HTMLParagraphElement, 129 React.HTMLAttributes<HTMLParagraphElement> 130 >(({ className, ...props }, ref) => { 131 const { formDescriptionId } = useFormField(); 132 133 return ( 134 <p 135 ref={ref} 136 id={formDescriptionId} 137 className={cn( 138 "text-[0.8rem] text-slate-500 dark:text-slate-400", 139 className, 140 )} 141 {...props} 142 /> 143 ); 144 }); 145 FormDescription.displayName = "FormDescription"; 146 147 const FormMessage = React.forwardRef< 148 HTMLParagraphElement, 149 React.HTMLAttributes<HTMLParagraphElement> 150 >(({ className, children, ...props }, ref) => { 151 const { error, formMessageId } = useFormField(); 152 const body = error ? String(error?.message) : children; 153 154 if (!body) { 155 return null; 156 } 157 158 return ( 159 <p 160 ref={ref} 161 id={formMessageId} 162 className={cn( 163 "text-[0.8rem] font-medium text-red-500 dark:text-red-900", 164 className, 165 )} 166 {...props} 167 > 168 {body} 169 </p> 170 ); 171 }); 172 FormMessage.displayName = "FormMessage"; 173 174 export { 175 useFormField, 176 Form, 177 FormItem, 178 FormLabel, 179 FormControl, 180 FormDescription, 181 FormMessage, 182 FormField, 183 };