React输入掩码问题:用户输入时替换占位符的手机号输入框
问题
需要创建一个带有特殊输入掩码的手机号输入框,占位符格式为(___) ___-____。用户输入时,占位符文本需逐步被输入内容替换,例如:(12_) ___-____(123) 4__-____(123) 456-7___
当前代码存在两处问题:无法正确处理退格键,且光标位置控制异常。格式化函数如下:
export const formatTelphoneNumber = (value: string): string => { const phoneNumber = getDigitsOnly(value) const areaCode = phoneNumber.substring(0, 3) const nextThree = phoneNumber.substring(3, 6) const lastFour = phoneNumber.substring(6, 10) const formattedAreaCode = `${areaCode}${"_".repeat(3 - areaCode.length)}` const formattedNextThree = `${nextThree}${"_".repeat(3 - nextThree.length)}` const formattedLastFour = `${lastFour}${"_".repeat(4 - lastFour.length)}` return `(${formattedAreaCode}) ${formattedNextThree}-${formattedLastFour}` }
解决方案
1. 修复退格键逻辑
原函数仅提取输入中的纯数字并拼接掩码,完全忽略了掩码符号(括号、空格、横杠)的存在。当用户退格时,如果光标停在掩码符号位置,或者删除的是掩码符号,原逻辑无法识别这种操作,导致退格后数字内容没有正确减少,甚至出现光标混乱。
解决思路:
- 区分用户操作是删除数字还是掩码符号:如果光标在掩码符号区域,仅调整光标位置,不修改数字内容;如果在数字区域,再删除对应数字并重新格式化。
- 通过映射光标位置与数字索引,精准定位需要删除的数字位。
2. 精准控制光标位置
格式化后的手机号字符串长度固定为14位((xxx) xxx-xxxx),用户输入或退格时,光标容易被默认逻辑跳到末尾或错误位置。需要建立光标位置与数字索引的映射关系,确保操作后光标停在正确的数字输入位。
核心映射规则
格式化字符串的结构与位置类型:
索引: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 字符: ( x x x ) x x x - x x x x 类型: M D D D M D D D M D D D D
(M=掩码位,D=数字位)
- 输入数字时:若光标在掩码位,自动跳转到下一个数字位;输入完成后,光标移动到下一个有效数字位(跳过掩码位)。
- 退格时:若光标在掩码位,先移动到前一个数字位再执行删除;删除后,光标停留在当前数字位的前一位(若前一位是掩码位则继续向前)。
优化后的完整代码
工具函数与格式化逻辑
// 提取输入中的纯数字 const getDigitsOnly = (value: string): string => { return value.replace(/\D/g, ''); }; // 根据光标位置计算对应的数字索引(忽略掩码位) const getDigitIndexFromCursor = (cursorPos: number): number => { const maskIndexes = [0, 4, 8]; // 掩码字符的索引:( ) - let digitCount = 0; for (let i = 0; i < cursorPos; i++) { if (!maskIndexes.includes(i)) digitCount++; } return digitCount; }; // 根据数字索引计算对应的光标位置 const getCursorFromDigitIndex = (digitIndex: number): number => { if (digitIndex === 0) return 1; if (digitIndex <= 3) return digitIndex + 1; if (digitIndex <= 6) return digitIndex + 2; return digitIndex + 3; }; // 格式化手机号并返回新的光标位置 export const formatPhoneWithCursor = ( value: string, cursorPos: number, isBackspace: boolean ): { formatted: string; newCursorPos: number } => { let digits = getDigitsOnly(value); const maxDigits = 10; // 处理退格操作 if (isBackspace) { const targetDigitIndex = getDigitIndexFromCursor(cursorPos); if (targetDigitIndex > 0) { digits = digits.slice(0, targetDigitIndex - 1); } } // 生成带占位符的格式化字符串 const areaCode = digits.slice(0, 3).padEnd(3, '_'); const middleSegment = digits.slice(3, 6).padEnd(3, '_'); const lastSegment = digits.slice(6, 10).padEnd(4, '_'); const formatted = `(${areaCode}) ${middleSegment}-${lastSegment}`; // 计算新的光标位置 let newCursor = getCursorFromDigitIndex(digits.length); if (!isBackspace && digits.length < maxDigits) { newCursor = Math.min(newCursor + 1, getCursorFromDigitIndex(digits.length + 1)); } return { formatted, newCursorPos: newCursor }; };
输入组件实现
import { useState, useRef } from 'react'; const PhoneNumberInput = () => { const [inputValue, setInputValue] = useState('(___) ___-____'); const inputRef = useRef<HTMLInputElement>(null); const handleBackspace = (e: React.KeyboardEvent<HTMLInputElement>) => { e.preventDefault(); const cursorPos = inputRef.current?.selectionStart || 0; const { formatted, newCursorPos } = formatPhoneWithCursor(inputValue, cursorPos, true); setInputValue(formatted); setTimeout(() => { inputRef.current?.setSelectionRange(newCursorPos, newCursorPos); }, 0); }; const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const cursorPos = inputRef.current?.selectionStart || 0; const { formatted, newCursorPos } = formatPhoneWithCursor(e.target.value, cursorPos, false); setInputValue(formatted); setTimeout(() => { inputRef.current?.setSelectionRange(newCursorPos, newCursorPos); }, 0); }; return ( <input ref={inputRef} value={inputValue} onChange={handleInputChange} onKeyDown={(e) => e.key === 'Backspace' && handleBackspace(e)} maxLength={14} type="tel" /> ); }; export default PhoneNumberInput;
内容的提问来源于stack exchange,提问作者Michael Pugh
相关产品推荐
相关产品推荐

