|
import * as React from "react" |
|
import * as LabelPrimitive from "@radix-ui/react-label" |
|
import { Slot } from "@radix-ui/react-slot" |
|
import { |
|
Controller, |
|
ControllerProps, |
|
FieldPath, |
|
FieldValues, |
|
FormProvider, |
|
useFormContext, |
|
} from "react-hook-form" |
|
|
|
import { cn } from "@/lib/utils" |
|
import { Label } from "@/components/ui/label" |
|
|
|
const Form = FormProvider |
|
|
|
type FormFieldContextValue< |
|
TFieldValues extends FieldValues = FieldValues, |
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> |
|
> = { |
|
name: TName |
|
} |
|
|
|
const FormFieldContext = React.createContext<FormFieldContextValue>( |
|
{} as FormFieldContextValue |
|
) |
|
|
|
const FormField = < |
|
TFieldValues extends FieldValues = FieldValues, |
|
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues> |
|
>({ |
|
...props |
|
}: ControllerProps<TFieldValues, TName>) => { |
|
return ( |
|
<FormFieldContext.Provider value={{ name: props.name }}> |
|
<Controller {...props} /> |
|
</FormFieldContext.Provider> |
|
) |
|
} |
|
|
|
const useFormField = () => { |
|
const fieldContext = React.useContext(FormFieldContext) |
|
const itemContext = React.useContext(FormItemContext) |
|
const { getFieldState, formState } = useFormContext() |
|
|
|
const fieldState = getFieldState(fieldContext.name, formState) |
|
|
|
if (!fieldContext) { |
|
throw new Error("useFormField should be used within <FormField>") |
|
} |
|
|
|
const { id } = itemContext |
|
|
|
return { |
|
id, |
|
name: fieldContext.name, |
|
formItemId: `${id}-form-item`, |
|
formDescriptionId: `${id}-form-item-description`, |
|
formMessageId: `${id}-form-item-message`, |
|
...fieldState, |
|
} |
|
} |
|
|
|
type FormItemContextValue = { |
|
id: string |
|
} |
|
|
|
const FormItemContext = React.createContext<FormItemContextValue>( |
|
{} as FormItemContextValue |
|
) |
|
|
|
const FormItem = React.forwardRef< |
|
HTMLDivElement, |
|
React.HTMLAttributes<HTMLDivElement> |
|
>(({ className, ...props }, ref) => { |
|
const id = React.useId() |
|
|
|
return ( |
|
<FormItemContext.Provider value={{ id }}> |
|
<div ref={ref} className={cn("space-y-2", className)} {...props} /> |
|
</FormItemContext.Provider> |
|
) |
|
}) |
|
FormItem.displayName = "FormItem" |
|
|
|
const FormLabel = React.forwardRef< |
|
React.ElementRef<typeof LabelPrimitive.Root>, |
|
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> |
|
>(({ className, ...props }, ref) => { |
|
const { error, formItemId } = useFormField() |
|
|
|
return ( |
|
<Label |
|
ref={ref} |
|
className={cn(error && "text-destructive", className)} |
|
htmlFor={formItemId} |
|
{...props} |
|
/> |
|
) |
|
}) |
|
FormLabel.displayName = "FormLabel" |
|
|
|
const FormControl = React.forwardRef< |
|
React.ElementRef<typeof Slot>, |
|
React.ComponentPropsWithoutRef<typeof Slot> |
|
>(({ ...props }, ref) => { |
|
const { error, formItemId, formDescriptionId, formMessageId } = useFormField() |
|
|
|
return ( |
|
<Slot |
|
ref={ref} |
|
id={formItemId} |
|
aria-describedby={ |
|
!error |
|
? `${formDescriptionId}` |
|
: `${formDescriptionId} ${formMessageId}` |
|
} |
|
aria-invalid={!!error} |
|
{...props} |
|
/> |
|
) |
|
}) |
|
FormControl.displayName = "FormControl" |
|
|
|
const FormDescription = React.forwardRef< |
|
HTMLParagraphElement, |
|
React.HTMLAttributes<HTMLParagraphElement> |
|
>(({ className, ...props }, ref) => { |
|
const { formDescriptionId } = useFormField() |
|
|
|
return ( |
|
<p |
|
ref={ref} |
|
id={formDescriptionId} |
|
className={cn("text-sm text-muted-foreground", className)} |
|
{...props} |
|
/> |
|
) |
|
}) |
|
FormDescription.displayName = "FormDescription" |
|
|
|
const FormMessage = React.forwardRef< |
|
HTMLParagraphElement, |
|
React.HTMLAttributes<HTMLParagraphElement> |
|
>(({ className, children, ...props }, ref) => { |
|
const { error, formMessageId } = useFormField() |
|
const body = error ? String(error?.message) : children |
|
|
|
if (!body) { |
|
return null |
|
} |
|
|
|
return ( |
|
<p |
|
ref={ref} |
|
id={formMessageId} |
|
className={cn("text-sm font-medium text-destructive", className)} |
|
{...props} |
|
> |
|
{body} |
|
</p> |
|
) |
|
}) |
|
FormMessage.displayName = "FormMessage" |
|
|
|
export { |
|
useFormField, |
|
Form, |
|
FormItem, |
|
FormLabel, |
|
FormControl, |
|
FormDescription, |
|
FormMessage, |
|
FormField, |
|
} |
|
|