2
0

form.tsx 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  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. FormProvider,
  7. useFormContext,
  8. useFormState,
  9. type ControllerProps,
  10. type FieldPath,
  11. type FieldValues,
  12. } from "react-hook-form"
  13. import { cn } from "@/client/lib/utils"
  14. import { Label } from "@/client/components/ui/label"
  15. const Form = FormProvider
  16. type FormFieldContextValue<
  17. TFieldValues extends FieldValues = FieldValues,
  18. TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
  19. > = {
  20. name: TName
  21. }
  22. const FormFieldContext = React.createContext<FormFieldContextValue>(
  23. {} as FormFieldContextValue
  24. )
  25. const FormField = <
  26. TFieldValues extends FieldValues = FieldValues,
  27. TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
  28. >({
  29. ...props
  30. }: ControllerProps<TFieldValues, TName>) => {
  31. return (
  32. <FormFieldContext.Provider value={{ name: props.name }}>
  33. <Controller {...props} />
  34. </FormFieldContext.Provider>
  35. )
  36. }
  37. const useFormField = () => {
  38. const fieldContext = React.useContext(FormFieldContext)
  39. const itemContext = React.useContext(FormItemContext)
  40. const { getFieldState } = useFormContext()
  41. const formState = useFormState({ name: fieldContext.name })
  42. const fieldState = getFieldState(fieldContext.name, formState)
  43. if (!fieldContext) {
  44. throw new Error("useFormField should be used within <FormField>")
  45. }
  46. const { id } = itemContext
  47. return {
  48. id,
  49. name: fieldContext.name,
  50. formItemId: `${id}-form-item`,
  51. formDescriptionId: `${id}-form-item-description`,
  52. formMessageId: `${id}-form-item-message`,
  53. ...fieldState,
  54. }
  55. }
  56. type FormItemContextValue = {
  57. id: string
  58. }
  59. const FormItemContext = React.createContext<FormItemContextValue>(
  60. {} as FormItemContextValue
  61. )
  62. function FormItem({ className, ...props }: React.ComponentProps<"div">) {
  63. const id = React.useId()
  64. return (
  65. <FormItemContext.Provider value={{ id }}>
  66. <div
  67. data-slot="form-item"
  68. className={cn("grid gap-2", className)}
  69. {...props}
  70. />
  71. </FormItemContext.Provider>
  72. )
  73. }
  74. function FormLabel({
  75. className,
  76. ...props
  77. }: React.ComponentProps<typeof LabelPrimitive.Root>) {
  78. const { error, formItemId } = useFormField()
  79. return (
  80. <Label
  81. data-slot="form-label"
  82. data-error={!!error}
  83. className={cn("data-[error=true]:text-destructive", className)}
  84. htmlFor={formItemId}
  85. {...props}
  86. />
  87. )
  88. }
  89. function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
  90. const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
  91. return (
  92. <Slot
  93. data-slot="form-control"
  94. id={formItemId}
  95. aria-describedby={
  96. !error
  97. ? `${formDescriptionId}`
  98. : `${formDescriptionId} ${formMessageId}`
  99. }
  100. aria-invalid={!!error}
  101. {...props}
  102. />
  103. )
  104. }
  105. function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
  106. const { formDescriptionId } = useFormField()
  107. return (
  108. <p
  109. data-slot="form-description"
  110. id={formDescriptionId}
  111. className={cn("text-muted-foreground text-sm", className)}
  112. {...props}
  113. />
  114. )
  115. }
  116. function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
  117. const { error, formMessageId } = useFormField()
  118. const body = error ? String(error?.message ?? "") : props.children
  119. if (!body) {
  120. return null
  121. }
  122. return (
  123. <p
  124. data-slot="form-message"
  125. id={formMessageId}
  126. className={cn("text-destructive text-sm", className)}
  127. {...props}
  128. >
  129. {body}
  130. </p>
  131. )
  132. }
  133. export {
  134. useFormField,
  135. Form,
  136. FormItem,
  137. FormLabel,
  138. FormControl,
  139. FormDescription,
  140. FormMessage,
  141. FormField,
  142. }