Ant Design 4.0 的一些杂事儿 - maxLength 篇
source link: https://zhuanlan.zhihu.com/p/359265292
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Ant Design 4.0 的一些杂事儿 - maxLength 篇
开发过程中,不同组件对于同一种边界情况有时候会出现差别。这些并非有意为之,就好比盲人摸象,似乎从细节看每个都很合理,但是脱离出来又会发现很多矛盾的地方。
今天,我们就从一个属性 maxLength
说起。看看我们在这个属性上,到底遇到了多少个坑。
maxLength 是不是 single source of truth
第一反应,我们总是会认为当配置 maxLength
时,组件值展示应该按照这个值来截断。但是在业务中,我们发现这会导致展示值和实际值并不一致。举个例子,一个表单存在一个 TextArea,它设置了 maxLength
为 10
,但是从后来获取的初始值超过了这个数字:
<Form.Item name="comment" initialValue="Hello World">
<TextArea maxLength={5} />
</Form.Item>
直觉上看,TextArea 很明显应该截取后展示为 Hello
:
然而,当用户不修改该文本框时。Form 内 comment
的值将始终为 Hello World
,提交时就会把错误的值发送出去:
{
"comment": "Hello World"
}
我们也遇到了很多相关问题:
综上所述,在受控状态下。组件展示值应该跟随受控值,而非截取值。我们测试了一下原生组件的行为,发现是相同的设计:
(题外话:使用原生表单时,如果 textarea 设置了 maxlength
且值超出了宽度,表单会无法提交并提示 too long
的错误。)
因此, maxLength
的约束逻辑也很简单:
- 受控时,不生效
- 非受控时,按照
maxLength
约束展示值
const [value, setValue] = useState('');
const mergedValue = props.value ?? value.slice(0, maxLength);
<textarea
value={mergedValue}
onChange={e => {
const triggerValue = e.target.value.slice(0, maxLength);
setValue(triggerValue);
onChange?.(triggerValue);
}}
/>
emoji 之熵
上述代码看起来一帆风顺,但是其实并不是所有字符的 length 都为 1。emoji 就是如此:
当用户传入的字符串最后一个为 emoji 且正好超出 maxLength
时,截取就会导致乱码。比如把 一切为二变成 ?
。为了解决这个问题,需要将 emoji 作为一个字符来处理。好在 js 的 Array.from
正好可以满足该需求:
Array.from(' light');
// [" ", "l", "i", "g", "h", "t"]
因此,我们的截取逻辑改如下即可:
const triggerValue = [...e.target.value].slice(0, maxLength).join('');
输入法之熵
在搞定 emoji 后,一切仍然未完。当字符数接近 maxLength
时使用输入法时会遇到截取问题:
这是由于在输入过程中,总体字符数已经到达了 maxLength
限制,因而被截取导致 textarea 的 value
被强制设置成了中间状态。比如 maxLength
为 1
,而我们需要通过输入法输入 你
(ni):
n
:符合长度,触发onChange('n')
i
:value
为ni
,超出长度1
。被截取为z
并触发onChange('n')
- textarea 强制赋值
n
,输入法状态丢失
为了解决输入法问题,我们需要暂时允许超出 maxLength
的情况。因而我们监听了 onCompositionXXX
事件,当正在使用输入法时暂时不做截取操作:
const [value, setValue] = useState('');
const [compositing, setCompositing] = useState(false);
const mergedValue = props.value ?? value.slice(0, maxLength);
function triggerChange(e, compositing) {
let triggerValue = e.target.value;
if (compositing) {
triggerValue = [...triggerValue].slice(0, maxLength).join('');
}
setValue(triggerValue);
if (mergedValue !== triggerValue) {
onChange?.(triggerValue);
}
}
<textarea
value={mergedValue}
onCompositionStart={() => setComposting(true)}
onChange={e => {
triggerChange(e, compositing);
}}
onCompositionEnd={(e) => {
setComposting(false);
triggerChange(e, false);
}}
/>
完整的输入流程如下:
渲染输入以上是我们对 TextArea 的 maxLength
处理逻辑的简单描述,antd 中由于我们返回的是 event 对象,因而对 React 的 event 做了一些额外的注入操作以与原生事件保持相同行为。感兴趣的同学可以直接到 github 阅读相关源码进行了解,此处不再详述。
除了 TextArea 意外,我们也对 InputNumber 进行了类似的处理。现在在受控模式下,InputNumber 的 value
在超出 max
和 min
范围时也会按照受控显示以防止展示值与实际值不一致的问题。此外,我们还做了额外的样式处理来表示超出范围的数值展示:
最后,目前我们体验技术部正在招人。如果你对前端充满热情,对细节锱铢必较,欢迎来私信勾搭哦 ~
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK