-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathform.tsx
117 lines (95 loc) · 3.37 KB
/
form.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import type * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
import { type ComponentProps, type HTMLAttributes, createContext, useContext, useId } from "react";
import {
Controller,
type ControllerProps,
type FieldPath,
type FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form";
import { cn } from "./cn";
import { Label } from "./label";
const Form = FormProvider;
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
name: TName;
};
const FormFieldContext = createContext<FormFieldContextValue>({} as FormFieldContextValue);
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
...rest
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: rest.name }}>
<Controller {...rest} />
</FormFieldContext.Provider>
);
};
const useFormField = () => {
const fieldContext = useContext(FormFieldContext);
const itemContext = 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 = createContext<FormItemContextValue>({} as FormItemContextValue);
function FormItem({ className, dense = true, ...rest }: HTMLAttributes<HTMLDivElement> & { dense?: boolean }) {
const id = useId();
return (
<FormItemContext.Provider value={{ id }}>
<div className={cn("space-y-2", dense && "space-y-1", className)} {...rest} />
</FormItemContext.Provider>
);
}
function FormLabel({ className, ...rest }: ComponentProps<typeof LabelPrimitive.Root>) {
const { error, formItemId } = useFormField();
return <Label className={cn(error && "text-destructive", className)} htmlFor={formItemId} {...rest} />;
}
function FormControl({ ...rest }: ComponentProps<typeof Slot>) {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
return (
<Slot
id={formItemId}
aria-describedby={error ? `${formDescriptionId} ${formMessageId}` : `${formDescriptionId}`}
aria-invalid={!!error}
{...rest}
/>
);
}
function FormDescription({ className, ...rest }: HTMLAttributes<HTMLParagraphElement>) {
const { formDescriptionId } = useFormField();
return <p id={formDescriptionId} className={cn("text-[0.8rem] text-muted-foreground", className)} {...rest} />;
}
function FormMessage({ className, children, ...rest }: HTMLAttributes<HTMLParagraphElement>) {
const { error, formMessageId } = useFormField();
const body = error ? String(error?.message) : children;
if (!body) {
return null;
}
return (
<p id={formMessageId} className={cn("animate-head-shake text-destructive text-xs", className)} {...rest}>
{body}
</p>
);
}
export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };