Redux Form Input Masking (Currency + DOB)

At my new job at RateSetter, we are using Redux forms to help manage the state of one of our applications.

One of the first tasks I was presented with was to mask input on a Loan Amount Requested field. The amount had to be formatted as a currency, meaning, that as the user entered numbers, commas had to be inserted to provide a correct currency format. This would normally be quite easy, except it was a requirement to have what was displayed in the input in a different format to what was saved in Redux.

Luckily, Redux Form provides lifecycle callbacks that helped formatting, normalizing and parsing user input which made this separation possible.

Lifecycle

The Redux Form Lifecycle is as follows:

The methods in detail are described here.

The documentation gives a good overview of the lifecycle of the form, but doesn't provide any many examples of how the lifecycle callbacks work. Keep reading if you want to see two real world examples of how you would use these lifecycle callbacks!

Example 1 - Currency Masking

Currency masking is when you need to automatically apply commas to the user's input to give a correct currency format, for example: 1000000 -> 1,000,000:

It might also be the case that you want to remove the commas from the user's input before saving to your redux store. This is effectively creating a separate view for the input to what is in the Redux store => a formatted value for the user to see (1,000,000) and a normalized value in our store (1000000).

We will be using the format() and normalize() lifecycle methods provided to do this.

From the Redux Form documentation:

format():

Formats the value from the Redux store to be displayed in the field input. Common use cases are to format Numbers into currencies or Dates into a localized date format.

normalize():

A function to convert whatever value the user has entered into the value that you want stored in the Redux store for the field. For instance, if you want the value to be in all uppercase, you would pass value => value.toUpperCase().

Let's go ahead and pass our format() and normalize() props to our Field component like:

<Field
    name="amount"
    component="input"
    type="text"
    format={this.formatCurrency}
    normalize={this.normalizeCurrency}
/>

We need to create a format() input that will take the unformatted input, and add commas. You can choose from two different methods to convert the number into a currency format:

  1. new Intl.NumberFormat()
  2. Regex - /\B(?=(\d{3})+(?!\d))/g

Using new Inlt.NumberFormat() would have our format method look like:

formatAmount = (input) => {
    if (!input) return;

    // Remove all existing commas before converting
    // This is not required if the normalize method is implemented and already removing commas
    const cleanedInput = input.replace(/,/g , ''); 

    // Convert to currency format
    const convertedInput = new Intl.NumberFormat().format(cleanedInput);

    return convertedInput;
}

Using the Regex method would have our format() method look like:

formatAmount = (input) => {
    if (!input) return;

    return input
        .replace(/,/g , '')
        .replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

Both these currency format conversion methods were discovered on Stack Overflow here.

So whats happening here? Every time the user enters a character, the following will happen:

  1. The input value will be run through the normalize() method (which we haven't created yet)
  2. Value is saved in the Redux store
  3. Value is retrieved from the Redux store
  4. Value is run through the format() method, which converts to number into our currency format
  5. Formatted value is rendered in the input form and displayed to the user

You can also prevent non-digit inputs by adding the following to the top of your format() function:

if (isNaN(parseInt(input[input.length - 1], 10))) {
    return input.slice(0, -1);
}

Now we need to take this formatted input that the user sees and remove all the commas to be save in our store. Of course, this is optional and depends how you want to save the value.

Our normalize() method is simple, it will be:

normalizeAmount = (val) => {
    return val.replace(/,/g , '');
}

Tada, now the value in the form will appear formatted and the value in our store will be unformatted:

Please note, that if you did implement the normalize() method, remove the .replace(/,/g , '') function in your format() method.

That's it! That's how simple it is to control the formatting of user input with Redux Form.

You could also add filtering (prevention) of non-digit characters using Regex if you wish as well.

Example 2 - Date of Birth Masking

Now that we've created input masking for currency, let's also do it for a date of birth input. The input format we will be using is dd/mm/yy, ie: 25/11/1990. Our masking will automatically insert / where they are required. The outcome will look like:

Obviously, you can't see me not entering / but you'll have to believe me.

This is a bit easier for us, as we only need to implement one function - normalize(). That is, unless we wish to separate what the user sees and what is stored in Redux.

Our Field component will look like:

<Field
    name="dob"
    component="input"
    type="text"
    normalize={this.normalizeDob}
/>

Our normalize() method will be:

normalizeDob = (val, prevVal) => {
    // Prevent non-digit characters being entered
    if (isNaN(parseInt(val[val.length - 1], 10))) {
        return val.slice(0, -1);
    }

    // When user is deleting, this prevents immediate re-addition of '/' when it's deleted 
    if (prevVal && (prevVal.length >= val.length)) {
        return val;
    }

    // Add / at appropriate sections of the input
    if (val.length === 2 || val.length === 5) {
        val += '/';
    }

    // Prevent characters being entered after Dob is full
    if (val.length >= 10) {
        return val.slice(0, 10);
    }

    return val;
}

There is no validation on the date provided here, that's a tutorial for another time (I'm also sure there's plenty of examples about dob validation available as well).

Summary

Using the lifecycle methods provided to us by Redux Form, we saw two examples of how by using these methods we can easily format how we want to display and store input, even if they are different formats!