问题:在React项目研发的过程中遇到一个输入框问题,在ios手机使用原生输入法输入中文时,待选拼音的变化也会触发change事件,导致input框内会把你敲的拼音和你确定选择的汉字一起显示在输入框中。
环境:react 17.x ; 组件库Ant Design Mobile v5.17.1(对于TextArea,SearchBar,Input等组件都会存在该问题);
具体原因如下:引用了原作者的文章:
原因:在input的onChange中监听到input中输入的值的时候进行了正则的判断,而输入中文的拼音时候也会走onChange这个方法,到这个方法做正则的替换时候不知道现在输入的是拼音,直接当成字母来处理了,就出现了ios全键盘输入拼音时候还没有选择汉字拼音就直接转化成了字母。
效果:用ios原生输入 “吗”字时候:log日志展示,valuechange监听到了拼音以及两遍“吗”字,导致输入框内容展示出问题。(注:ZGDAa为初始值,可忽略不看)
解决方法:
使用组件原生属性的方法compositionstart+compositionend+input来判断直接输入的还是非直接直接输入
- onCompositionStart:非直接的文字输入时(键盘输入中文的拼音)
- onCompositionend:直接输入文字后(键盘选择真实的汉字)
- onChange:输入框内容变化时触发
触发时机:
onCompositionStart和onCompositionend只会在ios手机的原生输入法 输入中文时调用。如果输入英文时候,是不会触发onCompositionStartEnd和onCompositionStartStart事件的。如果input的value没有变化也不会触发onCompositionStartEnd事件。
onCompositionStart事件是在onChange之前触发,onCompositionEnd是在onChange之后触发。
注:在赋值时候 不要使用value,而需要使用defaultValue。因为当使用value时候,即作为受控组件,随着 输入数据的变化,input组件重新渲染,不再是之前的input,这时页面上每次输入内容的更新都带来整个组件的重新渲染,导致
onCompositionEnd事件没有被调用。(文字最后面附有 value赋值的错误案例 )
代码原理: 将Textarea封装为一个子组件,如果输入中文拼音时候,只有在选择完汉字时才调用父组件的valueChange方法将最新值传给父组件,父类进行相应数据的更新后 再把新值传递给子组件,子组件这时候更新Textarea标签所绑定的value值。即: 定义一个inputFlag,在用户开始中文拼音输入时,调用onCompositionStart方法 更新inputFlag为true,此时用户输入内容 不进行dataChange的更新,直到用户停止拼音输入选择了某个汉字后,触发onCompositionEnd方法,更新inputFlag为false,并将此时输入框的值传给父组件,调用onDataChange方法更新。
具体代码:
//子组件Children代码
import React, { FC, useContext, useEffect, useState } from 'react';
import { TextArea } from 'antd-mobile';
import styles from './index.less';
interface ChildrenProps {
inputData: string,
onDataChange?: ( newVal: string) => void;
}
const Children: FC<ChildrenProps> = props => {
//父组件传过来的值和回调方法
const {inputData, onDataChange } = props;
//组件内部定义的state
const [hasGetDefaultValue,setHasGetDefaultValue] = useState(false)
const [defaultValue,setDefaultValue] = useState('')
const [inputFlag ,setInputFlag] = useState(false)
useEffect(()=>{
if (inputData) {
if(!hasGetDefaultValue){
//只在第一次时候渲染该初始值,之后的value值由textarea非受控组件内部自己管理,这样可以避免 value值更新时整个组件重新渲染 导致onCompositionEnd事件未调用。
setDefaultValue(inputData)
}
setHasGetDefaultValue(true);
}
},[inputData]);
function onValueChange(value: string) {
console.log('onValueChange', value)
if (!inputFlag) {
if (onDataChange) {
onDataChange( value );
}
}
}
return (
<div className={styles.childrenWrapper}>
<div className={styles.inputWrapper}>
{
hasGetDefaultValue &&
<TextArea
style={{
'--font-size': '14px',
}}
maxLength={300}
showCount={true}
autoSize={{
minRows: 4,
maxRows: 20
}}
defaultValue={defaultValue}
onChange={onValueChange}
onCompositionStart={ (e) => {
console.log('onCompositionStart', e.target.value)
setInputFlag(true)
}}
onCompositionEnd={ (e) => {
console.log('onCompositionEnd', e.target.value)
setInputFlag(false)
if(onDataChange) {
onDataChange( e.target.value );
}
}}
placeholder={'请输入'}
/>
}
</div>
</div>
);
};
export default Children;
//父组件Father代码:
import React, { FC, useState } from 'react';
import Children from './Children'
import styles from './index.less';
import classNames from 'classnames';
const Father: FC<FatherProps> = props => {
const [inputData, setInputData] = useState('初始值')
function renderContent() {
return (
<Children
inputData={inputData}
onDataChange={(newVal)=> {
//更新父组件值,子组件重新渲染
setInputData(newVal)
}}
/>
);
}
return (
<div className={styles.FatherWrapper}>
<div className={styles.fatherContent}>
{renderContent()}
</div>
</div>
);
};
export default Father;
效果:
可以看到,默认初始值为“初始值”,中文输入“一”时候,打第一个拼音“y”时会调用onCompositionStart事件,此时输入框的值为“初始值”。同时也会触发onValueChange事件,检测到输入框值变化,新值为:“初始值y”。
再输入后续拼音直到选中 所选汉字时,触发onCompositionEnd事件,通过e.target.value拿到真实输入内容,即“初始值一”。
注:最初的错误代码:如果通过value的方式给它赋值,即直接将父组件传过来的data赋值给input组件,代码如下:
import React, { FC, useContext, useEffect, useState } from 'react';
import { TextArea } from 'antd-mobile';
import styles from './index.less';
interface ChildrenProps {
inputData: string,
onDataChange?: ( newVal: string) => void;
}
const Children: FC<ChildrenProps> = props => {
const {inputData, onDataChange } = props;
const [inputFlag ,setInputFlag] = useState(false)
function onValueChange(value: string) {
console.log('onValueChange', value)
if (!inputFlag) {
if (onDataChange) {
onDataChange( value );
}
}
}
return (
<div className={styles.childrenWrapper}>
<div className={styles.inputWrapper}>
{
<TextArea
style={{
'--font-size': '14px',
}}
maxLength={300}
showCount={true}
autoSize={{
minRows: 4,
maxRows: 20
}}
value={inputData}
onChange={onValueChange}
onCompositionStart={ (e) => {
console.log('onCompositionStart', e.target.value)
setInputFlag(true)
}}
onCompositionEnd={ (e) => {
console.log('onCompositionEnd', e.target.value)
setInputFlag(false)
if(onDataChange) {
onDataChange( e.target.value );
}
}}
placeholder={'请输入'}
/>
}
</div>
</div>
);
};
export default Children;
此时会发现,无法输入中文字符,原因就是 在每输入一个中文拼音时,都会触发组件的重新渲染,导致还未执行后续的onCompositionEnd事件,该组件就被重新渲染了。因此没有来得及走onDataChange方法。导致输入框内容始终为“初始值”三个字。