Легкий кастомный компонент маскировщика номера телефона и email на React+TypeScript для Российского формата

15
22 октября 2025
Легкий кастомный компонент маскировщика номера телефона и email на React+TypeScript для Российского формата

Доброго времени суток! В современной веб-разработке часто возникает необходимость ввода контактных данных — телефона или 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-элемент input
  • lastInputValue - предыдущее значение для отслеживания изменений

Ключевые функции

  • applyPhoneMask - форматирует номер телефона по российскому стандарту
  • handleChange - основной обработчик изменений в поле ввода
  • handlePhoneInput - специализированный обработчик для телефонных номеров
  • handlePaste - обработчик вставки данных с автоматическим определением типа
  • handleFocus - управление фокусом и позицией курсора

Заключение

MaskedInput предоставляет удобное решение для ввода контактной информации в русскоязычном формате. Я не выкладываю его в виде библиотеки и проекта, потому что в этом нет необходимости, компонент на самом деле достаточно легкий и в то же время функциональный, вы можете модифицировать его в соответствии со своими потребностями.

Комментарии 0

Нет элементов для отображения

Только зарегистрированные пользователи могут оставлять комментарии Войти

Рекомендуем
Привет, мир! или давайте знакомиться с веб-технологиями
64
09 сентября 2025

Веб-разработка — это как собрать сложный, но невероятно красивый пазл

BlockifyPHP — безопасность структуры данных блочного редактора
48
09 сентября 2025

Доброго времени суток! Хочу поделиться радостной новостью! Недавно я успешно завершил разработку и довел до производственной готовности прое...