input
2023年07月18日
一、认识
浏览器内置的 <input>
组件 允许你渲染不同类型的表单输入框。
二、语法
2.1 受控
export default function App() {
const [v,setV] = useState("");
return <input value={v} onChange={(e)=> setV(e.target.value)}></input>
}
2.2 非受控
export default function App() {
const ref = useRef("");
const handleChange = (e)=>{
console.log(e.target.value)
}
return <input defaultValue={ref.current} onChange={handleChange}></input>;
}
三、属性
四、事件
五、实践
5.1 状态自治
当你使用受控输入框时,每次按键都会设置 state
。如果包含 state
的组件重新渲染了大型树形结构,这可能会变得很慢。因此, 可以将 Input
的状态自我管理, 这样可以显著提高性能
import { useState } from "react";
function Input() {
const [text, setText] = useState("");
return <input value={text} onChange={(e) => setText(e.target.value)}></input>;
}
export default function App() {
console.log("渲染");
return (
<>
<Input />
<div>无关部分</div>
</>
);
}
5.2 中文输入
问题描述:
在使用 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;
}
}
}