6

Ant Design 4.0 的一些杂事儿 - maxLength 篇

 3 years ago
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.
neoserver,ios ssh client

Ant Design 4.0 的一些杂事儿 - maxLength 篇

《豆酱》漫画作者

开发过程中,不同组件对于同一种边界情况有时候会出现差别。这些并非有意为之,就好比盲人摸象,似乎从细节看每个都很合理,但是脱离出来又会发现很多矛盾的地方。

今天,我们就从一个属性 maxLength 说起。看看我们在这个属性上,到底遇到了多少个坑。

maxLength 是不是 single source of truth

第一反应,我们总是会认为当配置 maxLength 时,组件值展示应该按照这个值来截断。但是在业务中,我们发现这会导致展示值和实际值并不一致。举个例子,一个表单存在一个 TextArea,它设置了 maxLength10 ,但是从后来获取的初始值超过了这个数字:

<Form.Item name="comment" initialValue="Hello World">
  <TextArea maxLength={5} />
</Form.Item>

直觉上看,TextArea 很明显应该截取后展示为 Hello

v2-9327ad535bb535a23d7fd850241d3f13_720w.jpg

然而,当用户不修改该文本框时。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 时使用输入法时会遇到截取问题:

https://github.com/ant-design/ant-design/issues/28940​github.com
v2-d1385216707c24a67ff44b74a0ee7b9d_b.jpg
上面为 TextArea,下面为原生 textarea

这是由于在输入过程中,总体字符数已经到达了 maxLength 限制,因而被截取导致 textarea 的 value 被强制设置成了中间状态。比如 maxLength1 ,而我们需要通过输入法输入 (ni):

  1. n :符合长度,触发 onChange('n')
  2. i : valueni,超出长度 1 。被截取为 z 并触发 onChange('n')
  3. 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 在超出 maxmin 范围时也会按照受控显示以防止展示值与实际值不一致的问题。此外,我们还做了额外的样式处理来表示超出范围的数值展示:

最后,目前我们体验技术部正在招人。如果你对前端充满热情,对细节锱铢必较,欢迎来私信勾搭哦 ~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK