Ant Design 4.0 的一些杂事儿 - maxLength 篇
- Ant Design 4.0 的一些杂事儿 - Overflow 篇
- Ant Design 4.0 的一些杂事儿 - VirtualList 篇
- Ant Design 4.0 的一些杂事儿 - Space 篇
- Ant Design 4.0 的一些杂事儿 - CI 篇
- Ant Design 4.0 的一些杂事儿 - Table 篇
- Ant Design 4.0 的一些杂事儿 - Form 篇
- Ant Design 4.0 的一些杂事儿 - Select 篇
开发过程中,不同组件对于同一种边界情况有时候会出现差别。这些并非有意为之,就好比盲人摸象,似乎从细节看每个都很合理,但是脱离出来又会发现很多矛盾的地方。
今天,我们就从一个属性
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);