react-input
2023年09月14日
一、中文输入
问题描述:
在使用 React
绑定 input
输入框的 onChange
方法时,如果使用中文输入法(或者其他输入法),会出现一个问题:还在输入拼音的时候,onChange
方法已经触发了,如下,即输入过程就已经触发了多次 onChange
方法。如果 onChange
方法有较为复杂的逻辑,就可能会带来一些用户体验或者逻辑的问题。
解决方案:
通过使用 compositionEvent
事件控制中文输入标识, 根据中文输入标识决定是否触发 onChange
。具体为: 通过监听输入法开始输入到结束的事件,即是去监听compositionstart
和compositionend
方法,通过设置一个变量,在两个方法里面设置 true/false
,来判断是否处在中文输入拼音这个过程当中,如果是,则不触发 onChange
后续事件。
兼容处理:
-
谷歌浏览器:
compositionstart -> onChange -> compositionend
,onChange
事件先于compositionend
事件触发, 因此我们需要针对谷歌浏览器的compositionend
结束后手动调用onChange
处理函数。 -
其他浏览器:
compositionstart -> compositionend -> onChange
具体实现
- App.tsx
- Input.tsx
- Input.scss
import { useState } from 'react';
import {Input} from './components/Input/input';
function App() {
const [value, setValue] = useState('');
const [error,setError] = useState('');
const handleInputOnChange = (e: string, len: number) => {
setValue(e);
if(len >= 20){
setError("xx 超过20个字符");
}
};
const handleInputOnBlur = () => {
if(value.length === 0){
setError("请输入xx");
}
}
return (
<div style={{width: '300px'}}>
<Input
limit={20}
value={value}
error={error}
limitType="character"
onBlur={handleInputOnBlur}
onChange={handleInputOnChange}
/>
</div>
);
}
export default App;
import './input.scss';
import React, { memo, useRef } from 'react';
export enum LimitType {
Text = 'text', // 按文本长度计算字数
Character = 'character' // 按字符数计算字数
}
export interface InputProps {
value: string;
type?: string;
error?: string;
limit?: number;
className?: string;
disabled?: boolean;
placeholder?: string;
limitType?: LimitType;
limitTextColor?: string;
limitActiveTextColor?: string;
onChange: (value: string, len: number) => void;
onBlur?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onFocus?: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export const getCharacterTextLength = (text: string) => {
let length = 0;
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
length += 0.5;
} else {
length += 1;
}
}
return length;
};
export const Input = memo(function Input(props: InputProps) {
const {
value,
limit,
error,
onChange,
type = 'text',
disabled = false,
className = '',
placeholder = '',
limitTextColor = '#666',
limitType = LimitType.Text,
limitActiveTextColor = '#dd4e40'
} = props;
const isComposing = useRef(false);
const getTextLength = (v: string) => {
const text = v.trim();
if (limitType === LimitType.Text) {
return text.length;
}
return getCharacterTextLength(text);
};
const getLimitText = (text: string, limit = Infinity) => {
let length = 0;
let result = '';
if (limitType === LimitType.Text) {
length = text.length;
result = text.substring(0, limit);
} else {
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i);
if (charCode >= 0 && charCode <= 128) {
length += 0.5;
} else {
length += 1;
}
if (length > limit) {
break;
}
result += text[i];
}
}
return { result, length };
};
const onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (!limit) {
onChange(e.target.value, getTextLength(e.target.value));
return;
}
const limitResult = getLimitText(e.target.value, limit);
let text = e.target.value;
if (!isComposing.current) {
text = limitResult.result;
}
onChange(text, limitResult.length);
};
const onCompositionEnd = () => {
isComposing.current = false;
if (!!limit) {
const { result, length } = getLimitText(value, limit);
onChange(result, length);
}
};
const renderMessageOfLimit = () => {
if (limit == undefined) {
return null;
}
const textLength = Math.min(Math.floor(getTextLength(value)),limit);
return (
<div
className="bolawen-input__message__limit"
style={{ color: limitTextColor }}
>
<span
style={{
color:
textLength === limit ? limitActiveTextColor : limitTextColor
}}
>
{textLength}
</span>
/{limit}
</div>
);
};
const renderMessage = () => {
if (!error && !limit) {
return null;
}
return (
<div className="bolawen-input__message">
<div className="bolawen-input__message__error">{error}</div>
{renderMessageOfLimit()}
</div>
);
};
return (
<div className={`bolawen-input ${className}`}>
<input
className="bolawen-input__input"
type={type}
value={value}
disabled={disabled}
placeholder={placeholder}
onBlur={props.onBlur}
onFocus={props.onFocus}
onChange={onValueChange}
onCompositionEnd={onCompositionEnd}
onCompositionStart={() => (isComposing.current = true)}
/>
{renderMessage()}
</div>
);
});
.bolawen-input {
position: relative;
.bolawen-input__input {
box-sizing: border-box;
width: 100%;
padding-right: 8px;
padding-left: 8px;
line-height: 30px;
background-color: #fff;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
.bolawen-input__message {
gap: 12px;
display: flex;
margin-top: 8px;
align-items: center;
justify-content: space-between;
.bolawen-input__message__error {
flex: 1;
width: 100%;
line-height: 20px;
color: #dd4e40;
}
.bolawen-input__message__limit {
flex: 0;
white-space: nowrap;
}
}
}