/ ui / src / components / FormNumericInput.tsx
FormNumericInput.tsx
  1  import {
  2    NumberInput,
  3    NumberInputField,
  4    NumberInputProps,
  5    NumberInputFieldProps,
  6    Box,
  7    FormControl,
  8    FormErrorMessage,
  9    FormHelperText,
 10    FormLabel,
 11    FormControlProps,
 12    BoxProps,
 13  } from '@chakra-ui/react';
 14  import { Controller, UseFormReturn, useFormContext } from 'react-hook-form';
 15  import {
 16    toPercent,
 17    toDollars,
 18    toFloat,
 19    parseLocaleFloat,
 20  } from 'utils/displayFunctions';
 21  
 22  interface FormInputProps
 23    extends Omit<NumberInputProps, 'value' | 'onChange' | 'onBlur'> {
 24    name: string;
 25    label: string;
 26    description?: string;
 27    inputFieldProps?: NumberInputFieldProps;
 28    control: UseFormReturn['control'] | any;
 29    formatAs?: 'percent' | 'float' | 'currency';
 30    formatAsDecimals?: number;
 31    controlVariant?: FormControlProps['variant'];
 32    wrapperProps?: Omit<BoxProps, 'children'>;
 33  }
 34  
 35  const getFormatters = ({
 36    formatAs,
 37    formatAsDecimals = 1,
 38  }: Pick<FormInputProps, 'formatAs' | 'formatAsDecimals'>) => {
 39    let format: NumberInputProps['format'], parse: NumberInputProps['parse'];
 40    switch (formatAs) {
 41      case 'percent':
 42        format = (val) => toPercent(Number(val), formatAsDecimals);
 43        parse = (val) => String(parseFloat(val) / 100);
 44        break;
 45      case 'currency':
 46        format = (val) => toDollars(Number(val), formatAsDecimals);
 47        parse = parseLocaleFloat;
 48        break;
 49      case 'float':
 50        format = (val) => toFloat(Number(val), formatAsDecimals);
 51        parse = parseLocaleFloat;
 52        break;
 53      default:
 54        format = (val) => val;
 55        parse = (val) => val;
 56        break;
 57    }
 58    return { format, parse };
 59  };
 60  
 61  const FormNumericInput = ({
 62    control,
 63    name,
 64    description,
 65    label,
 66    id,
 67    step,
 68    precision,
 69    maxWidth = 235,
 70    formatAs,
 71    formatAsDecimals = 1,
 72    controlVariant,
 73    wrapperProps,
 74    ...rest
 75  }: FormInputProps) => {
 76    const ctx = useFormContext();
 77    const state = ctx.getFieldState(name);
 78    const { format, parse } = getFormatters({ formatAs, formatAsDecimals });
 79  
 80    return (
 81      <Box maxW={680} my={5} {...wrapperProps}>
 82        <Controller
 83          control={control}
 84          name={name}
 85          render={({
 86            field: { onChange, onBlur, name, value },
 87            fieldState: { error },
 88          }) => (
 89            <FormControl isInvalid={!!error} variant={controlVariant}>
 90              <FormLabel htmlFor={id}>{label}</FormLabel>
 91              <FormHelperText>{description}</FormHelperText>
 92              <Box maxWidth={maxWidth} mt={5}>
 93                <NumberInput
 94                  onChange={(_, valueAsNumber) => {
 95                    onBlur();
 96                    onChange(valueAsNumber);
 97                  }}
 98                  name={name}
 99                  onBlur={onBlur}
100                  value={value}
101                  step={step}
102                  precision={precision}
103                  format={format}
104                  parse={parse}
105                  {...rest}
106                >
107                  <NumberInputField />
108                </NumberInput>
109              </Box>
110              <FormErrorMessage>{state?.error?.message}</FormErrorMessage>
111            </FormControl>
112          )}
113        />
114      </Box>
115    );
116  };
117  
118  export default FormNumericInput;