Доброго времени суток! В современной веб-разработке часто возникает необходимость ввода контактных данных — телефона или email. Представляю универсальный компонент MaskedInput, который обеспечивает удобный ввод и форматирование как телефонных номеров в российском формате, так и email адресов.
Цель заключалась в том, чтобы заменить громоздкие библиотеки маскировки, которые содержали большое количество ненужного кода, который не использовался в реальных проектах.
Возможности компонента
- Три режима работы: только телефон, только email, или комбинированный режим
- Автоматическое определение типа ввода в комбинированном режиме
- Интеллектуальное форматирование российских номеров телефонов
- Поддержка вставки (paste) с автоматическим определением типа данных
- Полная типобезопасность благодаря TypeScript
Как использовать
Базовое использование
<MaskedInput
mode="phoneOrEmail"
onChange={(value, type) => {
console.log('Введенное значение:', value);
console.log('Тип данных:', type);
}}
placeholder="Введите телефон или email"
/>Режим только для телефона
<MaskedInput
mode="phone"
onChange={(value) => setPhone(value)}
placeholder="+7 (9__) ___-__-__"
/>Режим только для email
<MaskedInput
mode="phone"
onChange={(value) => setPhone(value)}
placeholder="+7 (9__) ___-__-__"
/>Интеграция с формами
Поддержка стандартных форм или работа с библиотеками форм
<MaskedInput
mode="phone"
className="input"
onChange={(value) => {
// Или так к прмеру для библиотеки Formik
setFieldValue('phone', value);
}}
name="phone"
placeholder="+7 (9__) ___-__-__"
/>Особенности форматирования телефонов
Компонент автоматически форматирует номер по шаблону: +7 (XXX) XXX-XX-XX
Специальные случаи:
- Ввод + преобразуется в +7 (
- Ввод 7 преобразуется в +7 (
- Ввод 8 преобразуется в +7 (
- Ввод 9 преобразуется в +7 (9
- В других случаях, когда используется phoneOrEmail, это приводит к переключению в режим email.
При вводе первой цифры после кода страны автоматически добавляется 9, если она не была введена, что соответствует российскому стандарту мобильных номеров.
Полный код компонента
import React, { useState, useEffect, useRef } from 'react';
type MaskType = 'phone' | 'email' | 'none';
type InputMode = 'phone' | 'email' | 'phoneOrEmail';
interface MaskedInputProps {
onChange: (value: string, type: MaskType) => void;
value?: string;
placeholder?: string;
mode?: InputMode;
[inputProps: string]: unknown;
}
const MaskedInput: React.FC<MaskedInputProps> = ({
onChange,
value = '',
placeholder = 'Введите телефон или email',
mode = 'phoneOrEmail',
...inputProps
}) => {
const [inputValue, setInputValue] = useState(value);
const [maskType, setMaskType] = useState<MaskType>('none');
const [displayValue, setDisplayValue] = useState(value);
const inputRef = useRef<HTMLInputElement>(null);
const lastInputValue = useRef(value);
// Определяем тип маскировки на основе ввода и режима
useEffect(() => {
if (inputValue.length === 0) {
setMaskType('none');
return;
}
if (mode === 'phone') {
setMaskType('phone');
return;
}
if (mode === 'email') {
setMaskType('email');
return;
}
// Режим 'phoneOrEmail'
if (['+', '7', '8', '9', '0'].includes(inputValue[0])) {
setMaskType('phone');
} else {
setMaskType('email');
}
}, [inputValue, mode]);
// Обновляем отображаемое значение при изменении типа маски или значения
useEffect(() => {
if (maskType === 'phone') {
setDisplayValue(applyPhoneMask(inputValue));
} else {
setDisplayValue(inputValue);
}
}, [inputValue, maskType]);
// Применяем маскировку для телефона
const applyPhoneMask = (value: string): string => {
let cleaned = value.replace(/\D/g, '');
// Обработка вставки номера телефона
if (cleaned.startsWith('8')) {
cleaned = '7' + cleaned.slice(1);
}
if (!cleaned.startsWith('7') && cleaned.length > 0) {
cleaned = '7' + cleaned;
}
let result = '+7 (';
if (cleaned.length > 1) {
result += cleaned.slice(1, 4);
}
if (cleaned.length > 4) {
result += ') ' + cleaned.slice(4, 7);
}
if (cleaned.length > 7) {
result += '-' + cleaned.slice(7, 9);
}
if (cleaned.length > 9) {
result += '-' + cleaned.slice(9, 11);
}
return result;
};
// Обработчик изменения значения
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
// Определяем, было ли это действие вставкой
const isPaste = newValue.length - lastInputValue.current.length > 1 ||
(lastInputValue.current === '' && newValue.length > 1);
if (mode === 'phoneOrEmail' && maskType === 'phone') {
if ((newValue.trim() === '+7')) {
setInputValue('');
setDisplayValue('');
setMaskType('email');
onChange('', 'email');
lastInputValue.current = '';
return;
}
if (isPaste) {
// Обработка вставки
handlePaste(newValue);
return;
}
}
// Обрабатываем ввод для телефона
if (maskType === 'phone') {
handlePhoneInput(newValue);
} else {
// Обработка email или обычного ввода
setInputValue(newValue);
onChange(newValue, maskType);
lastInputValue.current = newValue;
}
};
// Обработчик ввода для телефона
const handlePhoneInput = (newValue: string) => {
let digits = newValue.replace(/\D/g, '');
// Обработка обычного ввода для телефона
if (digits.startsWith('8')) {
digits = '7' + digits.slice(1);
}
if (!digits.startsWith('7') && digits.length > 0) {
digits = '7' + digits;
}
// Автоматически добавляем 9 после кода страны, если ввод начинается не с 9
if (digits.length === 2 && digits[1] !== '9') {
// Если ввели первую цифру после 7 и это не 9, то ставим 9 ПЕРЕД введенной цифрой
digits = digits[0] + '9' + digits[1];
} else if (digits.length > 2 && lastInputValue.current.replace(/\D/g, '').length === 1 && digits[1] !== '9') {
// Если это первая цифра после 7 и она не 9, ставим 9 перед ней
digits = digits[0] + '9' + digits.slice(1);
}
// Ограничиваем длину номера (11 цифр: 7 + 10)
if (digits.length > 11) {
digits = digits.slice(0, 11);
}
setInputValue(digits);
onChange(digits, 'phone');
lastInputValue.current = digits;
};
// Обработчик вставки
const handlePaste = (pastedValue: string) => {
// Очищаем вставленное значение от всех нецифровых символов, кроме @ для email
const cleanedPaste = pastedValue.replace(/[^\d@a-zA-Z.]/g, '');
// Проверяем, похоже ли вставленное значение на email
const looksLikeEmail = cleanedPaste.includes('@') ||
(/[a-zA-Z]/.test(cleanedPaste) && !/^\d+$/.test(cleanedPaste));
if (looksLikeEmail) {
// Если похоже на email, переключаемся в режим email
setInputValue(pastedValue);
setMaskType('email');
onChange(pastedValue, 'email');
} else {
// Если похоже на телефон, обрабатываем как телефон
let digits = pastedValue.replace(/\D/g, '');
if (digits.startsWith('8')) {
digits = '7' + digits.slice(1);
} else if (!digits.startsWith('7') && digits.length > 0) {
digits = '7' + digits;
}
// Автоматически добавляем 9 после кода страны, если ввод начинается не с 9
if (digits.length >= 2 && digits[1] !== '9') {
digits = digits[0] + '9' + digits.slice(1);
}
// Ограничиваем длину номера
if (digits.length > 11) {
digits = digits.slice(0, 11);
}
setInputValue(digits);
setMaskType('phone');
onChange(digits, 'phone');
}
lastInputValue.current = pastedValue;
};
// Обработчик фокусировки
const handleFocus = () => {
if (inputRef.current) {
inputRef.current.focus();
// Устанавливаем курсор в конец текста
setTimeout(() => {
if (inputRef.current) {
const length = inputRef.current.value.length;
inputRef.current.setSelectionRange(length, length);
}
}, 0);
}
};
// Обработчик keyDown для отслеживания backspace
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Backspace') {
lastInputValue.current = inputValue;
}
};
return (
<div onClick={handleFocus}>
<input
ref={inputRef}
type="text"
value={displayValue}
onChange={handleChange}
onKeyDown={handleKeyDown}
placeholder={placeholder}
{...inputProps}
/>
</div>
);
};
export default MaskedInput;Что за что отвечает в коде
Типы и интерфейсы
MaskType- определяет возможные типы маскировки: телефон, email или отсутствие маскиInputMode- режимы работы компонента: только телефон, только email, или оба вариантаMaskedInputProps- интерфейс пропсов компонента
Основные состояния
inputValue- сырое значение без форматированияmaskType- текущий тип маскировкиdisplayValue- отформатированное значение для отображенияinputRef- ссылка на DOM-элемент inputlastInputValue- предыдущее значение для отслеживания изменений
Ключевые функции
applyPhoneMask- форматирует номер телефона по российскому стандартуhandleChange- основной обработчик изменений в поле вводаhandlePhoneInput- специализированный обработчик для телефонных номеровhandlePaste- обработчик вставки данных с автоматическим определением типаhandleFocus- управление фокусом и позицией курсора
Заключение
MaskedInput предоставляет удобное решение для ввода контактной информации в русскоязычном формате. Я не выкладываю его в виде библиотеки и проекта, потому что в этом нет необходимости, компонент на самом деле достаточно легкий и в то же время функциональный, вы можете модифицировать его в соответствии со своими потребностями.
Нет элементов для отображения