Using abstractions for form fields when using Formik

Viewed 170

I am using formik as a library to handle form data.

For some fields, I would like to use a different underlying type, e.g. I use decimal.js-light for handling of decimal values*.

So in my form values, I'd like my decimal types to be represented by this type. However, when the user is typing, it should still treat the input as string rather than a number. A classic example is if the user wants to change "1000" to "2000", if they delete the "1" and enter a "2", the value would become "20", if the input is treated as a number during typing.

We also need to handle the user's locale (the locale depends on the country of the tenant that the user is a member of, not the browser settings).

So when the form is loaded, I need to convert the decimal to a localized string. When I submit the form, I need parse the string in the user's locale, which is not trivial.

There are also some dependent fields, e.g. I have a quantity and amount field - and we calculate the a total as quantity*amount (using decimal.js). So now I have the rules for localized parsing 3 different places, when submitting, when validating input, and when calculating the total.

Are there patterns for formik for dealing with a different underlying abstraction of the type of data you're working with?

* We use decimal.js because JavaScript doesn't handle decimal precision, e.g. 0.1+0.2 does not equal 0.3. For many scenarios this doesn't matter, but in my application, I need decimal precision.

1 Answers

So, in the end, we do have 3 sets of data types:

  • numbers that we get from the server and that should be saved (either in number or string format if it is required to be saved with high precision)
  • formiks fields of type Decimal
  • and, formatted strings to be represented to the end user.

Representation of the data can be easily extracted to the separate encapsulated layer.

One of the formik's Field arguments - is component. You can use it to create a separate component that will play only with the field value, format it as it should be, and show it to the end user. So, the formatted value will live only inside that component. It would not be exposed outside. There is a pretty library(react-number-format) that can deal with it in a sophisticated way.

It may be done like the following:

import NumberFormat from 'react-number-format'
import { Field } from 'formik'

const FormattedPrice = () => {
  return <Field component={NumberFormat} thousandSeparator="," {/* ...otherProps */} />
}

or:

import NumberFormat from 'react-number-format'
import { useField } from 'formik'

const FormattedPrice2 = () => {
  const [{value}] = useField('price')

  return (
    <NumberFormat
      thousandSeparator=","
      prefix="$"
      value={value}
      {/* ...otherProps */}
    />
  )
}

If you use material UI, its TextInput component also has a prop component where you can pass your custom decorator or use an existing solution of react-number-format. I guess there should be all cases handled that you may expect to have. And documentation is pretty exhaustive.

It means, that you don't need to manage the actual state of formatted data, all you have to do - is provide some configuration.

String localization would also be the responsibility of that decorator component.

After this - we will have raw values in our formiks context. And, all that is left - is to write some mappers to convert numbers to Decimals and provider them as an initialValues to formik and vice versa - map Decimals to numbers somewhere in the onSubmit after data is validated. That part may be extracted to separate helper class to handle all in one place and make his functions self-annihilating. The approximate interface of that class:

import Decimal from 'decimal.js-light'

type BackendResponseData = {
  price: number
  // other fields
}

type FormValues = {
  price: Decimal
  // other fields
}

interface DataConverter {
  storedDataToFormValues(storedData: BackendResponseData) => FormValues
  formValuesToStoredData(formValues: FormValues) => BackendResponseData
}
Related