Regex based controlled Input in React.js

Ankit kaushik
3 min readMar 31, 2024

Isn’t it very common to have input having some restrictions, examples include:

  • allow only at most 2 decimals in a number
  • only alphanumeric characters
  • only positive numbers
  • not allow trailing spaces
  • preventing whitespaces in the beginning
  • and so many patterns more.

Today, we are going to build a directive / utility / High Order Component to handle such scenarios effectively with ease which depicts extensibility and robustness.

Hands on Keyboard and floating blurred characters
Typing the input on Keyboard

How are we going to determine whether the input text is valid as per the provided pattern

const isValid = (pattern: string, text) => {
return new RegExp(pattern).test(text);
}

For the common patterns in your application you can also have it defined to be reused. Something like this:

const categories = {
decimals_2: '^[0-9]+(\\.[0-9]{0,2}){0,1}$',
numbers: '^[0-9]d{9}'
};

// passing defined one or the custom
const isValid = (pattern: string, text) => {
return new RegExp(categories[pattern] || pattern).test(text);
}

Along with the pattern, let us also include the support for a callbackFn the user could provide optionally to additionally run some checks on the newValue for its validity.

const isValid = 
(pattern, text, isInvalidFn = (newValue) => false) => {
if (isInvalidFn(text)) {
return false;
}

return new RegExp(categories[pattern] || pattern).test(text);
}

The most common strategy that could come to you is preventDefault on keydown event if it doesn’t match the pattern, and you are brilliant if that hit you. Here, we are gonna leverage the same technique to build our utility.

const onKeyDown = (event) => {
// it is not event.target.value
// as that's gonna be the old value
const value = ??

if (!isValid(value)) {
event.preventDefault();
}
}

But! Wait a second. What if the user hitting enter / return / backspace / or any meta key? Those keystrokes should pass through as it doesn’t affect our input. Lets handle that:

const shouldPass = (e: KeyboardEvent) => {
switch (true) {
case e.metaKey:
case e.ctrlKey:
case e.altKey:
case e.key?.length !== 1: return true;
default: return isValidText(e, e.key, pattern);
}
}

Now the challenge is, how do we get the new value formed after we allow that key stroke. Here’s the algorithm, that also takes takes care of the edge case when the cursor might be in the middle of the existing value or text.

const constructFutureValue = (event, newText = event.key) => {
const start = event.target.selectionStart;
const end = event.target.selectionEnd;
const oldValue = event.target.value;

const futureValue =
oldValue.substring(0, start) + newText + oldValue.substring(end);

return futureValue;
}

While constructing the future value we are using selectionStart or selectionEnd to get the cursor selected range that could be available only for the text based inputs such as: text, search, URL, tel, and password. So, we might want to warn or throw an error if directive is being applied to any non text based input. I chose to do it the following way, although other ways are also possible.

const validateInputType = (el: HTMLInputElement) => {
if (typeof el.selectionStart !== 'number') {
console.error(
'%c Cannot include permitInput for non-text input elements!',
'color: red; background: yellow; font-size: 16px;'
);
}
};

After the keydown event, shouldn’t we handle the paste as well and not allow user to paste any value that doesn’t fit the criteria. Now, we have to listen to the paste event as well, and in the handler the only change will be replacing event.key with event.clipboardData?.getData('text/plain') to get the new text fed.

const onPaste = (event: ClipboardEvent) => {
const newText = event.clipboardData?.getData('text/plain');

const futureValue = constructFutureValue(event, newText);

...
}

For React.js enthusiasts, You could have an HOC with these superpowers

// incase you want to attach just the props
export function permitInputProps(pattern, isInvalidFn = (value: string) => false) {
return {
type: 'text',
onKeyDown: attachKeyDown(pattern, isInvalidFn),
onPaste: attachPaste(pattern, isInvalidFn)
};
}

export function withPermitInput(
InputComponent,
pattern,
isInvalidFn = (value: string) => false
) {
return (props) => (
<InputComponent
type="text"
{...props}
{...permitInputProps(pattern, isInvalidFn)}
/>
);
}

Here is the Final Gist

You can see it in action here:

Stackblitz playground for the solution

I’ll be glad if you found it helpful. Thanks for reading, stay tuned for more of such content and help spread the good words.

Quality is not an act, it is a habit ~ Aristotle

--

--