Files
pitchfast/components/ui/form.tsx

219 lines
5.1 KiB
TypeScript

"use client";
import * as React from "react";
import {
Controller,
FormProvider,
useFormContext,
type ControllerProps,
type FieldPath,
type FieldValues,
type SubmitHandler,
type UseFormReturn,
} from "react-hook-form";
import { cn } from "@/lib/utils";
type FormProps<TFieldValues extends FieldValues> = Omit<
React.FormHTMLAttributes<HTMLFormElement>,
"onSubmit" | "children"
> & {
form: UseFormReturn<TFieldValues>;
onSubmit: SubmitHandler<TFieldValues>;
children: React.ReactNode;
};
const FormItemContext = React.createContext<{ id: string } | null>(null);
type FormFieldContextValue = {
name: string;
};
const FormFieldContext = React.createContext<FormFieldContextValue | null>(null);
const Form = <TFieldValues extends FieldValues>({
form,
onSubmit,
children,
className,
...props
}: FormProps<TFieldValues>) => {
return (
<FormProvider {...form}>
<form
className={cn("w-full space-y-4", className)}
onSubmit={form.handleSubmit(onSubmit)}
{...props}
>
{children}
</form>
</FormProvider>
);
};
const useFormField = () => {
const itemContext = React.useContext(FormItemContext);
const fieldContext = React.useContext(FormFieldContext);
const { getFieldState, formState, control } = useFormContext();
if (!itemContext || !fieldContext) {
throw new Error("useFormField must be used within a <FormField>.");
}
return {
control,
id: itemContext.id,
name: fieldContext.name,
...getFieldState(fieldContext.name, formState),
};
};
const FormField = <
TFieldValues extends FieldValues,
TName extends FieldPath<TFieldValues>,
>({
...props
}: Omit<ControllerProps<TFieldValues, TName>, "render"> & {
render: ControllerProps<TFieldValues, TName>["render"];
}) => {
return (
<FormFieldContext.Provider value={{ name: String(props.name) }}>
<Controller
control={props.control}
name={props.name}
render={props.render}
/>
</FormFieldContext.Provider>
);
};
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("grid gap-2", className)} {...props} />
</FormItemContext.Provider>
);
});
FormItem.displayName = "FormItem";
const FormLabel = React.forwardRef<
HTMLLabelElement,
React.LabelHTMLAttributes<HTMLLabelElement>
>(({ className, ...props }, ref) => {
const { error, id } = useFormField();
return (
<label
ref={ref}
htmlFor={props.htmlFor ?? id}
className={cn("text-sm leading-none font-medium", className)}
style={error ? { color: "var(--destructive)" } : undefined}
{...props}
/>
);
});
FormLabel.displayName = "FormLabel";
const getFormControlAriaDescribedBy = (fieldId: string, hasError: boolean) => {
const descriptionId = `${fieldId}-description`;
const messageId = `${fieldId}-message`;
if (hasError) {
return `${descriptionId} ${messageId}`;
}
return descriptionId;
};
const FormControl = React.forwardRef<
HTMLElement,
React.HTMLAttributes<HTMLElement>
>(({ className, children, ...props }, ref) => {
const { id, error } = useFormField();
const controlId = props.id ?? id;
const control = React.Children.only(children);
if (!React.isValidElement(control)) {
return null;
}
const typedControl = control as React.ReactElement<
React.ClassAttributes<unknown> & Record<string, unknown>
>;
const controlClassName = (typedControl.props as { className?: string })
.className;
return React.cloneElement(typedControl, {
id: controlId,
ref: ref,
className: cn("relative", className, controlClassName),
...props,
"aria-invalid": error ? "true" : "false",
"aria-describedby": getFormControlAriaDescribedBy(
controlId,
!!error?.message,
),
"aria-errormessage": error?.message ? `${controlId}-message` : undefined,
});
});
FormControl.displayName = "FormControl";
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { id } = useFormField();
return (
<p
ref={ref}
id={`${id}-description`}
className={cn("text-xs leading-5 text-muted-foreground", className)}
{...props}
/>
);
});
FormDescription.displayName = "FormDescription";
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { error, id } = useFormField();
if (!error?.message) {
return null;
}
return (
<p
ref={ref}
id={`${id}-message`}
className={cn("text-xs text-destructive", className)}
role="alert"
{...props}
>
{typeof error.message === "string" ? error.message : String(error.message)}
</p>
);
});
FormMessage.displayName = "FormMessage";
export {
Form,
useFormField,
FormField,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
};