form.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. "use client"
  2. import * as React from "react"
  3. import * as LabelPrimitive from "@radix-ui/react-label"
  4. import { Slot } from "@radix-ui/react-slot"
  5. import {
  6. Controller,
  7. ControllerProps,
  8. FieldPath,
  9. FieldValues,
  10. FormProvider,
  11. useFormContext,
  12. } from "react-hook-form"
  13. import { cn } from "@/lib/utils"
  14. import { Label } from "@/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, formState } = useFormContext()
  41. const fieldState = getFieldState(fieldContext.name, formState)
  42. if (!fieldContext) {
  43. throw new Error("useFormField should be used within <FormField>")
  44. }
  45. const { id } = itemContext
  46. return {
  47. id,
  48. name: fieldContext.name,
  49. formItemId: `${id}-form-item`,
  50. formDescriptionId: `${id}-form-item-description`,
  51. formMessageId: `${id}-form-item-message`,
  52. ...fieldState,
  53. }
  54. }
  55. type FormItemContextValue = {
  56. id: string
  57. }
  58. const FormItemContext = React.createContext<FormItemContextValue>(
  59. {} as FormItemContextValue
  60. )
  61. const FormItem = React.forwardRef<
  62. HTMLDivElement,
  63. React.HTMLAttributes<HTMLDivElement>
  64. >(({ className, ...props }, ref) => {
  65. const id = React.useId()
  66. return (
  67. <FormItemContext.Provider value={{ id }}>
  68. <div ref={ref} className={cn("space-y-2", className)} {...props} />
  69. </FormItemContext.Provider>
  70. )
  71. })
  72. FormItem.displayName = "FormItem"
  73. const FormLabel = React.forwardRef<
  74. React.ElementRef<typeof LabelPrimitive.Root>,
  75. React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
  76. >(({ className, ...props }, ref) => {
  77. const { error, formItemId } = useFormField()
  78. return (
  79. <Label
  80. ref={ref}
  81. className={cn(error && "text-destructive", className)}
  82. htmlFor={formItemId}
  83. {...props}
  84. />
  85. )
  86. })
  87. FormLabel.displayName = "FormLabel"
  88. const FormControl = React.forwardRef<
  89. React.ElementRef<typeof Slot>,
  90. React.ComponentPropsWithoutRef<typeof Slot>
  91. >(({ ...props }, ref) => {
  92. const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
  93. return (
  94. <Slot
  95. ref={ref}
  96. id={formItemId}
  97. aria-describedby={
  98. !error
  99. ? `${formDescriptionId}`
  100. : `${formDescriptionId} ${formMessageId}`
  101. }
  102. aria-invalid={!!error}
  103. {...props}
  104. />
  105. )
  106. })
  107. FormControl.displayName = "FormControl"
  108. const FormDescription = React.forwardRef<
  109. HTMLParagraphElement,
  110. React.HTMLAttributes<HTMLParagraphElement>
  111. >(({ className, ...props }, ref) => {
  112. const { formDescriptionId } = useFormField()
  113. return (
  114. <p
  115. ref={ref}
  116. id={formDescriptionId}
  117. className={cn("text-sm text-muted-foreground", className)}
  118. {...props}
  119. />
  120. )
  121. })
  122. FormDescription.displayName = "FormDescription"
  123. const FormMessage = React.forwardRef<
  124. HTMLParagraphElement,
  125. React.HTMLAttributes<HTMLParagraphElement>
  126. >(({ className, children, ...props }, ref) => {
  127. const { error, formMessageId } = useFormField()
  128. const body = error ? String(error?.message) : children
  129. if (!body) {
  130. return null
  131. }
  132. return (
  133. <p
  134. ref={ref}
  135. id={formMessageId}
  136. className={cn("text-sm font-medium text-destructive", className)}
  137. {...props}
  138. >
  139. {body}
  140. </p>
  141. )
  142. })
  143. FormMessage.displayName = "FormMessage"
  144. export {
  145. useFormField,
  146. Form,
  147. FormItem,
  148. FormLabel,
  149. FormControl,
  150. FormDescription,
  151. FormMessage,
  152. FormField,
  153. }