form.tsx 3.9 KB

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