FormField.tsx 2.8 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import { cloneElement, useId } from 'react';
  2. import type { CSSProperties, ReactElement, ReactNode } from 'react';
  3. import { Controller } from 'react-hook-form';
  4. import type { Control, ControllerProps, FieldValues, Path } from 'react-hook-form';
  5. import { Form } from 'antd';
  6. import { useTranslation } from 'react-i18next';
  7. import { normalizeAntdOnChange, type ValueProp } from './normalizeAntdOnChange';
  8. import { toDotted, type FieldName } from './toDotted';
  9. interface FormFieldTransform {
  10. input?: (value: unknown) => unknown;
  11. output?: (eventValue: unknown) => unknown;
  12. }
  13. export interface FormFieldProps<T extends FieldValues = FieldValues> {
  14. name: FieldName;
  15. control?: Control<T>;
  16. label?: ReactNode;
  17. tooltip?: ReactNode;
  18. extra?: ReactNode;
  19. valueProp?: ValueProp;
  20. transform?: FormFieldTransform;
  21. onAfterChange?: (value: unknown) => void;
  22. rules?: ControllerProps<T>['rules'];
  23. required?: boolean;
  24. noStyle?: boolean;
  25. className?: string;
  26. style?: CSSProperties;
  27. children: ReactElement;
  28. }
  29. export function FormField<T extends FieldValues = FieldValues>({
  30. name,
  31. control,
  32. label,
  33. tooltip,
  34. extra,
  35. valueProp = 'value',
  36. transform,
  37. onAfterChange,
  38. rules,
  39. required,
  40. noStyle,
  41. className,
  42. style,
  43. children,
  44. }: FormFieldProps<T>) {
  45. const { t } = useTranslation();
  46. const dottedName = toDotted(name) as Path<T>;
  47. const generatedId = useId();
  48. const explicitId = (children as ReactElement<{ id?: string }>).props.id;
  49. const fieldId = explicitId ?? generatedId;
  50. return (
  51. <Controller
  52. control={control}
  53. name={dottedName}
  54. rules={rules}
  55. render={({ field, fieldState }) => {
  56. const displayValue = transform?.input ? transform.input(field.value) : field.value;
  57. const help = fieldState.error?.message
  58. ? t(fieldState.error.message, { defaultValue: fieldState.error.message })
  59. : undefined;
  60. const childProps: Record<string, unknown> = {
  61. id: fieldId,
  62. [valueProp]: displayValue,
  63. onChange: (...args: unknown[]) => {
  64. const raw = normalizeAntdOnChange(args, valueProp);
  65. const next = transform?.output ? transform.output(raw) : raw;
  66. field.onChange(next);
  67. onAfterChange?.(next);
  68. },
  69. onBlur: field.onBlur,
  70. ref: field.ref,
  71. };
  72. return (
  73. <Form.Item
  74. label={label}
  75. htmlFor={label ? fieldId : undefined}
  76. tooltip={tooltip}
  77. extra={extra}
  78. required={required}
  79. validateStatus={fieldState.error ? 'error' : undefined}
  80. help={help}
  81. noStyle={noStyle}
  82. className={className}
  83. style={style}
  84. >
  85. {cloneElement(children as ReactElement<Record<string, unknown>>, childProps)}
  86. </Form.Item>
  87. );
  88. }}
  89. />
  90. );
  91. }
  92. export default FormField;