React.js. Компонент для ввода pin-кода
08.09.2021
Теги: Frontend • JavaScript • React.js • Web-разработка • Компонент • Практика
Небольшой компонент для ввода pin-кода из четырех цифр. Создает четыре input-поля и отслеживает, чтобы в каждом была только одна цифра. При вводе очередной цифры смещает фокус на следующее поле. Используя клавишу Backspace, можно удалить введенное значение, при втором нажатии — сместить фокус на предыдущее поле.
Родительский компонент хранит pin-код в состоянии:
import { useState } from 'react'; import PinCodeInput from './PinCodeInput'; const initDigits = ['', '', '', '']; export default function Form() { const [digits, setDigits] = useState(initDigits); return ( <form> <PinCodeInput digits={digits} changeHandler={setDigits} /> </form> ); }
Для начала отрисуем четыре input-поля и зададим для них css-стили:
import { useRef, useEffect } from 'react'; const inputStyle = { border: '2px solid #000', width: 30, height: 30, fontSize: 20, textAlign: 'center', margin: 5, }; export default function PinCodeInput(props) { const { digits, changeHandler } = props; return ( <div> {digits.map((digit, index) => ( <input key={index} style={inputStyle} value={digit} /> ))} </div> ); }
Это управляемый компонент, при вводе значения в поле — нужно вызывать changeHandler
:
/* .......... */ export default function PinCodeInput(props) { const { digits, changeHandler } = props; const handleChange = (index, newValue) => { const oldDigit = digits[index]; // старую цифру из поля ввода убираем, оставляя только новую const newDigit = newValue.trim().replace(oldDigit, ''); // теперь вызываем callback родителя, чтобы обовить digits const newDigits = [...digits]; // копия digits newDigits[index] = newDigit; changeHandler(newDigits); } return ( <div> {digits.map((digit, index) => ( <input key={index} style={inputStyle} value={digit} onChange={event => handleChange(index, event.target.value)} /> ))} </div> ); }
Нам нужно, чтобы пользователь вводил только цифры:
/* .......... */ export default function PinCodeInput(props) { /* .......... */ const handleChange = (index, newValue) => { const oldDigit = digits[index]; // старую цифру в поле ввода убираем, оставляя только новую const newDigit = newValue.trim().replace(oldDigit, ''); // если это не цифра, ничего не делаем, пока не будет цифры if (newDigit < '0' || newDigit > '9') return; // теперь вызываем callback родителя, чтобы обовить digits const newDigits = [...digits]; // копия digits newDigits[index] = newDigit; changeHandler(newDigits); } /* .......... */ }
После ввода цифры — смещаем фокус на следующее поле:
/* .......... */ export default function PinCodeInput(props) { const { digits, changeHandler } = props; const length = digits.length; // здесь будут ссылки на input-элементы const inputRefs = useRef([]); const handleChange = (index, newValue) => { const oldDigit = digits[index]; // старую цифру в поле ввода убираем, оставляя только новую const newDigit = newValue.trim().replace(oldDigit, ''); // если это не цифра, ничего не делаем, пока не будет цифры if (newDigit < '0' || newDigit > '9') return; // теперь вызываем callback родителя, чтобы обовить digits const newDigits = [...digits]; // копия digits newDigits[index] = newDigit; changeHandler(newDigits); // смещаем фокус на следующее поле для ввода следующей цифры if (index < length - 1) { inputRefs.current[index + 1].focus(); } else { // или убираем фокус, если это было последнее поле inputRefs.current[index].blur(); } } return ( <div> {digits.map((digit, index) => ( <input key={index} style={inputStyle} value={digit} onChange={event => handleChange(index, event.target.value)} ref={element => inputRefs.current[index] = element} /> ))} </div> ); }
Будем подсвечивать поля красным или зеленым:
/* .......... */ export default function PinCodeInput(props) { /* .......... */ // поля подсвечиваем красным (ошибка) или зеленым useEffect(() => { digits.forEach((value, index) => { if (value.match(/^[0-9]$/)) { inputRefs.current[index].style.backgroundColor = '#dfd'; } else { inputRefs.current[index].style.backgroundColor = '#fdd'; } }); }, [digits]); /* .......... */ }
При монтировании — установим фокус на первом поле:
/* .......... */ export default function PinCodeInput(props) { /* .......... */ // при монтировании компонента фокус на первом поле useEffect(() => { inputRefs.current[0].focus(); }, []); /* .......... */ }
Теперь добавим еще функционал, чтобы при первом нажатии клавиши Backspace удалялась введенная ранее цифра, а при повторном нажатии — фокус смещался на предыдущее поле.
import { useRef, useEffect } from 'react'; const inputStyle = { border: '2px solid #000', width: 30, height: 30, fontSize: 20, textAlign: 'center', margin: 5, }; export default function PinCodeInput(props) { const { digits, changeHandler } = props; const length = digits.length; // здесь будут ссылки на input-элементы const inputRefs = useRef([]); // при монтировании компонента фокус на первое поле useEffect(() => { inputRefs.current[0].focus(); }, []); // поля подсвечиваем красным (ошибка) или зеленым useEffect(() => { digits.forEach((value, index) => { if (value.match(/^[0-9]$/)) { inputRefs.current[index].style.backgroundColor = '#dfd'; } else { inputRefs.current[index].style.backgroundColor = '#fdd'; } }); }, [digits]); const handleKeyDown = (index, event) => { if (event.key === 'Backspace') { event.preventDefault(); if (digits[index].match(/^[0-9]$/)) { // если элемент массива digits содержит цифру, то при нажатии клавиши // вызываем callback-функцию родителя, чтобы записать пустую строку const newDigits = [...digits]; // копия digits newDigits[index] = ''; changeHandler(newDigits); } else { // элемент массива digits пустой, удалять нечего — так что надо сместить // фокус на предыдущее поле input — при условии, что это не первое поле if (index > 0) inputRefs.current[index - 1].focus(); } } } const handleChange = (index, newValue) => { const oldDigit = digits[index]; // старую цифру в поле ввода убираем, оставляя только новую const newDigit = newValue.trim().replace(oldDigit, ''); // если это не цифра, ничего не делаем, пока не будет цифры if (newDigit < '0' || newDigit > '9') return; // теперь вызываем callback родителя, чтобы обовить digits const newDigits = [...digits]; // копия digits newDigits[index] = newDigit; changeHandler(newDigits); // смещаем фокус на следующее поле для ввода следующей цифры if (index < length - 1) { inputRefs.current[index + 1].focus(); } else { // или убираем фокус, если это было последнее поле inputRefs.current[index].blur(); } } return ( <div> {digits.map((digit, index) => ( <input key={index} style={inputStyle} value={digit} onChange={event => handleChange(index, event.target.value)} onKeyDown={event => handleKeyDown(index, event)} ref={element => inputRefs.current[index] = element} /> ))} </div> ); }
Очистка формы
Хотелось мы иметь возможность очистить pin-код. Кнопка очистки должна быть в компоненте Form
. Но после этого надо установить фокус на первое поле. А для этого нам нужен доступ из Form
к первому элементу <input>
внутри компонента PinCodeInput
.
import { useState } from 'react'; import PinCodeInput from './PinCodeInput'; const initDigits = ['', '', '', '']; export default function Form() { const [digits, setDigits] = useState(initDigits); const clear = (event) => { event.preventDefault(); setDigits(initDigits); } return ( <form> <PinCodeInput digits={digits} changeHandler={setDigits} /> <button onClick={event => clear(event)}>Очистить</button> </form> ); }
В компоненте PinCodeInput
мы уже получаем ссылки на все input-элементы. И нам надо просто в компоненте Form
создать переменную для хранения ссылки на первый input-элемент, передать ее через пропсы компоненту PinCodeInput
и там присвоить ей значение.
import { useState, useRef } from 'react'; import PinCodeInput from './PinCodeInput'; const initDigits = ['', '', '', '']; export default function Form() { const [digits, setDigits] = useState(initDigits); const firstInputRef = useRef(); const clear = (event) => { event.preventDefault(); setDigits(initDigits); firstInputRef.current.focus(); } return ( <form> <PinCodeInput digits={digits} changeHandler={setDigits} firstInputRef={firstInputRef} /> <button onClick={event => clear(event)}>Очистить</button> </form> ); }
/* .......... */ export default function PinCodeInput(props) { const { digits, changeHandler, firstInputRef } = props; /* .......... */ // при монтировании компонента фокус на первое поле useEffect(() => { inputRefs.current[0].focus(); firstInputRef.current = inputRefs.current[0]; }, []); /* .......... */ }
React еще предлагает использовать для этих целей forwardRef
и useImperativeHandle
. Получается запутанно, но давайте рассмотрим и этот способ. Первым делом создаем в компоненте Form
переменную для хранения ссылки на первый input-элемент и передаем ее дочернему компоненту через специальный атрибут ref
.
import { useState, useRef } from 'react'; import PinCodeInput from './PinCodeInput'; const initDigits = ['', '', '', '']; export default function Form() { const [digits, setDigits] = useState(initDigits); const firstInputRef = useRef(); const clear = (event) => { event.preventDefault(); setDigits(initDigits); firstInputRef.current.focus(); } return ( <form> <PinCodeInput digits={digits} changeHandler={setDigits} ref={firstInputRef} /> <button onClick={event => clear(event)}>Очистить</button> </form> ); }
Дочерний компонент PinCodeInput
нужно обернуть в forwardRef
:
import { useRef, useEffect, useImperativeHandle, forwardRef } from 'react'; /* .......... */ function PinCodeInput(props, refer) { const { digits, changeHandler } = props; const length = digits.length; // здесь будут ссылки на input-элементы const inputRefs = useRef([]); useImperativeHandle(refer, () => ({ focus: () => inputRefs.current[0].focus() })); // при монтировании компонента фокус на первое поле useEffect(() => { inputRefs.current[0].focus(); }, []) /* .......... */ } export default forwardRef(PinCodeInput);
Хук useImperativeHandle
в качестве первого аргумента принимает объект refer
, который мы создали в компоненте Form
и передали в PinCodeInput
через специальный атрибут ref
. В качестве второго аргумента хук принимает функцию, которая должна вернуть объект, который станет значением свойства current
этого объекта. В свойство current
(которое является объектом) мы записываем одно-единственное свойство focus
— нам просто больше ничего не нужно.
В результате всех этих манипуляций переменная firstInputRef
, которую мы определили в компоненте Form
, будет иметь вид:
{ current: { focus: () => inputRefs.current[0].focus() } }
В свойство current
мы могли бы записать и другие свойства, например style
, чтобы изменять css-стили первого input-элемента:
useImperativeHandle(refer, () => ({ focus: () => inputRefs.current[0].focus(), style: inputRefs.current[0].style }));
{ current: { focus: () => inputRefs.current[0].focus(), style: CSSStyleDeclaration {...} } }
Поиск: JavaScript • React.js • Web-разработка • Frontend • Компонент • Практика