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;