2
0

form.tsx 3.8 KB

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