5
(2)

In the article I wrote previously I showed how to create a 6 digit random code and copy it to clipboard. This is kind of a sequel to that article, because once the code is copied somewhere it needs to be pasted also. That somewhere is the OTP input fields, where you can write the digits manually or just paste it inside. Let’s get straight with the requirements before we start:

The code

VerificationCodeInputs.tsx

import { useState } from 'react';
import VerificationCodeInput from './VerificationCodeInput';
import styled from "styled-components";


const Title= styled.h2`
    font-size: 30px;
`
const VerificationCode = styled.div`
    display: flex;
    column-gap: 10px;
    input {
        width: 25px;
        line-height: 1;
        font-size: 25px;
        padding: 5px;
        text-align: center;
        border: 1px solid #ccc;
        &:focus {
            outline:none;
            border-color: red
        }
    }
`
interface OtpProps {
    codeLength: number;
}

const VerificationCodeInputs = ({codeLength}: OtpProps) => {
    const emptyCode = Array(codeLength).fill('');
    const [code, setCode] = useState(emptyCode);

    const handleCode = (ev: React.ChangeEvent<HTMLInputElement>, value: string, index: number) => {
        const newCode = [...code];
        const remainingFields = codeLength - index;
        const newValue = value.length ? value.split('', remainingFields) : [''];
        const newValueSize = value.length ? value.length : 1; 
        const target = ev.currentTarget as HTMLInputElement;

        newCode.splice(index, newValueSize , ...newValue);
        setCode(newCode);

        if(value.length && value.length < codeLength  && index !== codeLength - 1) {
            (target.nextElementSibling as HTMLInputElement || null).focus();
        }
        if(value.length === codeLength) {
            target.blur();
        }
        
    }

    const handleKey = (ev: React.KeyboardEvent<HTMLInputElement>, index: number) => {
        const target = ev.currentTarget as HTMLInputElement;
        if(ev.key === 'Backspace' && target.value === '' && index) {
            (target.previousElementSibling as HTMLInputElement || null).focus();
        }
        if(ev.key === 'ArrowLeft') {
            const prevElement = target.previousElementSibling as HTMLInputElement || null;
            if(prevElement) prevElement.focus();
        }
        if(ev.key === 'ArrowRight') {
            const nextElement = target.nextElementSibling as HTMLInputElement || null;
            if(nextElement) nextElement.focus();
        }
    }

    return (
        <>
            <Title>Verification code</Title>
            <VerificationCode>
                {
                    code.map((char, index) => ( 
                        <VerificationCodeInput 
                            key={index} 
                            handleCode={handleCode} 
                            handleKey={handleKey}
                            char={char} 
                            index={index} 
                            maxLength={codeLength}
                        />
                    ))
                }
            </VerificationCode>
        </>
    );
}

export default VerificationCodeInputs;

VerificationCodeInput.tsx

interface CodeInputProps {
    handleCode: (ev: React.ChangeEvent<HTMLInputElement>, value: string, index: number) => void;
    handleKey: (ev: React.KeyboardEvent<HTMLInputElement>, index: number) => void;
    char: string;
    index: number;
    maxLength: number
}

const VerificationCodeInput = ({handleCode, handleKey, char, index, maxLength}: CodeInputProps) => {
    const handleChange = (ev: React.ChangeEvent<HTMLInputElement>) => {
        const pattern = /^\d*$/;
        const target = ev.currentTarget as HTMLInputElement;
        const isValidChar = pattern.test(target.value);

        if(!isValidChar) return;

        handleCode(ev, target.value, index);
    }
    const handleFocus = (ev: React.FocusEvent<HTMLInputElement>) => {
        (ev.currentTarget as HTMLInputElement).select();
    }

    return (
        <input 
            type="text" 
            inputMode="numeric" 
            autoComplete="one-time-code"
            onChange={handleChange}
            onKeyDown={(ev) => handleKey(ev, index)}
            value={char}
            onFocus={handleFocus}
            maxLength={maxLength}
         />
    )
}

export default VerificationCodeInput;

Let me explain

The only prop that we add to our component is the codeLength, that will tell the component how many inputs to create and how many digits to expect.

After this we create our interface or type alias for the prop, which is always a number and create an array with empty strings, having the amount of items inside that we specified in the codeLength prop. We will store this array as the initial value of the code state. Shortly we will map through this array from the state and create at each iteration a child component with an input inside:

The key props is mandatory for React to identify each instance of this component separately, the char prop is actually the value of the input, maxLength is the limitation of the input to accept a maximum of “n” numbers as values, I mean there is no need to copy/paste 12 digits in an input of we have only 6 inputs.

What does the child component do with the props?

First we need to take care of the types of the props on this component. Our child component returns this JSX:

The input is of type text, inputMode(numeric) will instruct handheld devices to show the numeric keyboard only, autoComplete(one-time-only) is the instruction to fill in the values if it is sent via SMS.

You can learn about the “one-time-only” autocomplete value here:

We make this form of inputs controlled by adding a value attribute that is dependent on the character stored in the “code” state but modified on every onChange event in the handleChange function. We also used the onKeyDown event handler for the Backspace and Left, Right arrows interaction.

The handleChange function

I choose to use the onChange event handler since we need to target not only the keyboard inputs(keydown, keyup, keypress) but also the paste mouse or keyboard events and this is covered by the onChange. When this event is triggered the handleChange function executes, having as prop the event itself(ev) and it’s type is the React.ChangeEvent on a <HTMLInputElement>.

We need to permit only digits inside of the input field so the regular expression in the pattern constant validates the input and the pattern is a string that starts with 0 or more digits. This way we cover also the no value scenario(0 number of digits), for example, when we remove the value or initially when we have empty values.

Also I used the ev.currentTarget because it is more exact even if in our case it does not matter that much, it is still a good practice. When an event happens there is always event bubbling happening and the target can be also a child element instead of our element and than errors happen. The currentTarget get’s exactly the element that holds the event handler.

If the input passed the validation, we call the function handleCode passed down from the parent component and send up to that parent some arguments. The arguments are the event itself, the value of the input and the index of the child component. This way we can add to the item with that certain index in our array a new value.

First we create a clone of our state by spreading it into a new array. After this we have to take care of 2 scenarios: The first when you introduce manually numbers one-by-one and the second when you paste instantly for example 6 digits(into one input) and our function needs to distribute it in all our inputs. How can we do that pasted value distribution?

Well in the newValue constant we check if the value is empty(we just deleted a value) then just return an array with an empty string. If it has some digits(1 or more) then split this string and separate every digit. When we have an array of digits, we can map through it and on each item we create again the child component(the input) with the new value. That is what basically is happening.

To replace only the affected array items I used the splice method. The first argument is the starting point, which is the index of the changed input, the second argument is the number of deleted items(starting for the position given in the 1st argument). In our case this is or the input’s value length or if it’s an empty value then let it be 1 instead of 0 otherwise the splice method won’t delete anything only add. The elements that will be the replacements will be spread out as the 3rd argument. Now we can reassign a new value to the state that will rerender all input elements(child components).

I added also some checks there. For example if our value’s length is equal with the maximum length(specified in codeLength prop) then after pasting it into the inputs it should loose focus since we pasted the code in.

Also when we type the digits by hand we need automatically to move the cursor to the next element:

  • value.length -> avoid triggering this action when you hit backspace, then the value is a falsy value, since length will be 0
  • value.length < codeLength -> this is how we identify that this is not a paste, but more a like a manual typing(we can use also the onPaste event if you want)
  • index !== codeLength – 1 -> when we are on the last input this action should not happen

So what is that remainingFields constant?

Since the split() method can have another argument besides the separator, the second argument represents in how many items can it divide the string into(maximum). When we try to paste value inside the second input or third and the pasted value has for example 6 digits, it will try to replace from that position(2nd or 3rd) 6 items, but we have less then 6 remaining from that position. That’s why we subtracted the index from the total length(codeLength).

I tried to paste 6 digits from 3rd input. Of course you can do another logic here and even if you paste this from another input then the first, it will still paste it from the first input. For that you only need to change this line:

newCode.splice(index, newValueSize , ...newValue);

Into this line:

Keyboard interraction

I’ve set up to focus on the previous element if backspace key is pressed. The event handler is the onKeyDown because while we press the backspace key down and the button did not came back up we have time to change the focused element to the previous sibling input and the backspace button when is up(onKeyUp) it will actually delete not the original but the input on it’s left. We tricked the system, didn’t we?

The ArrowLeft when pressed it will change the focus on the previous element and the ArrowRight button pressed will change the focus on the next element, if there is one(if it’s not the last input).

One last optimization that I did is that when you focus on an input it should select all content in that input, otherwise you always have to select the digit inside and replace it with a new digit. If we don’t apply this little improvement, you will have the cursor next to your digit and then your keypress will add to your digit not replace it.

That’s it guys, enjoy ! Oh and don’t forget: Add a comment, subscribe or press the like button or give the article a rating. You have so many options, just me a sign that you where here and you enjoyed yourself !

How useful was this post?

Click on a star to rate it!

Average rating 5 / 5. Vote count: 2

No votes so far! Be the first to rate this post.

Categorized in: