47

更多我对Antd的使用及思考,请参考:antd-doddle

扯点犊子

一晃眼,两个月过去了,自己从一家不大不小的屌丝公司跳到一家被具有纯正互联网血液的公司。从以前的围绕jQuery、Echarts为主技术栈开展工作,到现在以React、Antd为主技术栈开发业务;但不是所有的业务antd都能支持,所以有时得自己动手,在antd上做一层浅封装。
文章中提到的示例都可以在codeBox找到:codeBox

自定义表单组件

Antd的Form表单介绍一节中,提到过自定义表单控件。其实例是关于货币价值转换的,如下图所示:
image
当我们在我们的页面中需要频繁的用到某一个组合类型的组件,而Antd又不支持时,最好的做法就是对Antd组件做一层浅封装行成一个独立的组件,当然也可以使用html 自有的表单元素进行封装,只是这样做出来费事,且样式和整个页面没有那么容易统一。封装的注意事项在上面的截图中已经一一列出:核心就是value的处理,与onChange事件的支撑,接下来将以一个实例来操作说明。

一个带远程搜索的下拉选择组件

2018.12月更新:随着Select组件comobox模式在新的版本中被舍弃,和autoComplete组件的出现。这个组件也进行了重写。但整体逻辑没有改变,主要时改变了激活弹出框和关闭弹出框的逻辑。具体可参见我的github项目:React进阶
seri
这个组件的大致实现需求如上面动态图所示。产品需求就是需要一个编辑框,这个框在用户点击输入时,需要弹出一个搜索框,根据用户的输入远程搜索获取数据形成一个下拉列表,供用户选择。这在jquery时代,这是一个很常见的需求,也有很多的组件可选择,但在Antd的组件库中,没有完全匹配的,但有及其相似功能的,比如:

image

这个组件与产品的需求契合度已经达到了80%, 但是产品说了搜索输入框需要与编辑输入框分开,并且有明显的区别,ok,那就费点事,把Antd组件稍微做一下改变嘛。
image
所以简单分解一下,需要用到Input,Icon, Select这三种组件,具体实现可以查看SandBox上的源码及示例。说一下自己遇到的难点:

支持双向绑定

<FormItem type="inline" label="员工姓名">
  {getFieldDecorator('search',{
    initialValue: { name: 'Dom' }
  })(
    <OriginSearch {...modalProps} />
  )}
</FormItem>

在Antd的Form表单组件中,如果需要做数据双向绑定,就需要用到其提供的getFieldDecorator方法来装饰组件,而我们自己封装的组件要支持这个特性的话,如最开始提到的,我们需要使用onChange方法来触发装饰器值的同步。

在我们为一个组件添加了装饰器后,可以查看props明显发现多了id,onChange, value三个属性,value属性是用于获取我们在initialValue设定的值的,而onChange方法是用于同步值,实现双向绑定。所以在我们这个组件中,当用户从下拉框中选择一个选项后,我们需要调用onChange方法去同步值,代码如下所示:

handleSelect(value, option) {
  const { handleSelect } = this.props;
  const { seachRes } = this.state;
  // 初始化基础信息
  const selectValue = handleSelect(value, option, seachRes) || value;
  this.triggerChange(selectValue);
  this.setState({ value: selectValue });
  this.handleCloseSearch();
}
triggerChange(value) {
  const { onChange } = this.props;
  // 调用装饰器方法去同步选中后的值
  onChange && onChange(value);
}

点击组件以外的地方收起组件

这看似是个很容易实现的需求,但因为Antd所有的弹框组件都用了同一套方法,其弹框Dom树并不是挂载在Select输入框的父节点上,而是直接挂载在Body节点上,所以想用冒泡的机制来实现就不可能了。所以就和投机用了点击事件的节点名称来判断,看具体实现:

componentDidUpdate(prevProps, prevState) {
  const { isShowSearch } = this.state;
  const bodyNode = document.querySelector("body");
  if (isShowSearch !== prevState.isShowSearch) {
    // 状态切换的时候才改变状态
    if (isShowSearch) {
      document
        .querySelector(".js-origin-search .ant-select-search__field")
        .focus();
      bodyNode.addEventListener("click", this.handleChangeVisible);
    } else {
      bodyNode.removeEventListener("click", this.handleChangeVisible);
    }
  }
}
handleChangeVisible(event) {
  const { isShowSearch } = this.state;
  event = event || window.event;
  const elem = event.target;
  let inComponentClick = false;
  // 当搜索框框被打开时,点击空白处搜索框收起;由于antd的下拉列表是挂载在body下,而非搜索框节点下的某一子节点,所以
  // 无法采用阻止冒泡的方式来避免body下的click事件被响应,所以只有靠判断被点击的节点类,来判断body的click事件是否响应
  if (
    (this.searchInputElement && this.searchInputElement.contains(elem)) ||
    elem.className.indexOf("ant-select-dropdown") !== -1
  ) {
    inComponentClick = true;
  }
  // 当点击事件为非下拉列表选中事件,切搜索框为展开时,触发搜索框收起方法;
  !inComponentClick && isShowSearch && this.handleCloseSearch();
}

虽然这只是一次很简单的封装,但其包含的知识点还是非常多的。自己还封装过日期多选,日期选择增加至今,地址地区联合选择器这种,从实现上其实都是一个思路,在这一个SandBox项目中都能看到。

组件奇特使用方式(持续更新)

动态更新表单组件Required参数,但验证没有同步

加入现在有这样一个需求,用户需要选择自己的性别(男,女,其他),当用户选择其他时,下面说明项由非必填变为必填。这看似是一个很简单的需求,在用户选择其他时,将isRequired变量变为true就行了,看起来好像,大概,貌似成功了。但是自己在antd组件的应用上,发现,当你把isRequired置为true,label标签前面会加上一个*,但这只是一个假象,当你填上数据再删除时,antd组件这时并不会自动验证,并触发提示词显示。但是,这些都没有。所谓的isRequired置为true,并没有达到真正想要的效果,测试代码如下,我猜测antd的这个机制和他的initValue更新机制相似,只有在组件初始化的时候会设置一次初始值,后面都是组件内部state参数进行状态切换,原以为用resetFields可以解决,最后发现不能。但是巧的方法没有,不代表笨办法也没有。

<FormItem
  label="性别"
  labelCol={{ span: 5 }}
  wrapperCol={{ span: 12 }}
>
  {getFieldDecorator('gender', {
    rules: [{ required: true, message: 'Please select your gender!' }],
  })(
    <Select
      placeholder="Select a option and change input text above"
      onChange={this.handleSelectChange}
    >
      <Option value="male">male</Option>
      <Option value="female">female</Option>
    </Select>
  )}
</FormItem>
<FormItem
  label="说明"
  labelCol={{ span: 5 }}
  wrapperCol={{ span: 12 }}
>
  {getFieldDecorator('note', {
    rules: [{ required: isRequired, message: 'Please input your note!' }],
  })(
    <Input />
  )}
</FormItem>

最后想出的最好的解决办法就是动态销毁重新挂载这个组件,我们可以通过动态设定key值,来保证状态的同步。其实这是重新渲染了一个新的组件来替换。

<FormItem
  label="说明"
  key={isRequired}
  labelCol={{ span: 5 }}
  wrapperCol={{ span: 12 }}
> 

2019.01.06日更:
上面的问题,在自己使用3.9的版本上没有再出现了,但是好像又出现了一个更大的表单动态校验问题。看下图,需求和上面差不多:
2303313742-5c317658312cd_articlex
当选择启用时,原因必填。其他时,变为非必填。现在出现的情况时,必填时触发错误提醒,但将状态置为禁用时,label的必填属性虽然被重置了,但错误提醒仍然存在,有些奇怪。
基于上面的现象,我去基友社区搜了一下Issuse,果然时存在的:form动态校验问题,幸运的时,antd大佬也给出了解决方案:使用form.validateFields([key], { force: true })来解决

实践后的幡然醒悟

用了两个多月,其实Antd自己本身没啥坑,只是由于我们组现在使用的版本是2.9,但自己习惯于看3.x的版本文档,所以多次在一个地方徘徊好久,总以为是自己代码实现有问题,其实是2.9版本还没有实现。

总结一

在Select组件上,2.9与3.x就有较大的差异:

  1. Select 的option必须带有不同的Key值,且value值也不能有相同的,比如在远程搜索加载员工列表时,就会出现同名的情况,所以这时的value就不能只用名字,得用value加工号或则其他值来代替。
  2. Select 的 onchange事件在3.x版本以前回调函数只有value值,没有option回调参数。
  3. Select 的notFundContent属性可配置结合Spin实现加载动画,但在版本3以下,该配置对于comobox模式无效(其文档未对这个特性(Bug)做说明)。。。
  4. Select 的 onSelect事件在3.x以后也有较大改动,其option参数包含的内容作了很大调整,在2.9版本还可以通过option.props.index获取选择的索引,在3.x版本只能间接通过设置key为index,然后通过获取key值来获取index;
  5. Select 组件渲染出来的下拉列表是没有挂载在Select组件父节点上的,其是采用绝对定位,挂载在body节点上的。。。所有用父节点做筛选是无法获取的。

总结二

另外在表单组件自校验validator的使用上,有一个隐藏的少有人知的使用方法是:

<FormItem {...formItemLayout} label="确认密码">
    {
        getFieldDecorator('confirmPassword', {
            rules: [{
                    required: true,
                    message: '请再次输入以确认新密码',
                }, {
                    validator: this.handleConfirmPassword
                }],
        })(<Input type="password" />)
    }
</FormItem> 
handleConfirmPassword(rule, value, callback) {
    if (value && value.length < 5 ) {
        callback('长度不足');
        return;
    }
    // Note: 必须总是返回一个 callback,否则 validateFieldsAndScroll 无法响应
    callback()
}

总结三

当我们使用getFieldDecorator并用initialValue设定初始值时,当我们改变组件的值时,组件表现出的值也改变了,但这个值并不是initialValue设定的,其是组件内部的state值保持的,如果需要继续用initialValue来设定组件的值,我们需要调用resetFields方法使initialValue有效;

总结四:Table设置width无效

Antd组件个人觉得最好用的功能就是Table,其配合pagination可以直接实现前端分页,在有些使用场景可以大大提高使用体验。但是Table也有坑(其实也是css一个隐形知识点),就是有时你会发现你为一列设置了width,但是并没有鸟用。

{
  key: 'userId',
  name: '用户ID',
  value: 'asddsddsfsfsdfsdfsdfsfsfdsfsfsfsfsfddefgervwerbvw'
  width: 80
}

就像上面的这种数据,设置了80的宽度,但最后撑开差不多是300。最后的最后,记起了又一个css属性叫 word-break ,来历就是在浏览器中,纯数字或者纯字母的字符串,他的显示默认是不换行的,就算他已经超出了这一行的边际,就是这么叼的一个属性。所以Table也受这个影响,由于我要展示的内容中是纯字母,纵然我设置了width,依然没什么鸟用。需要设置与td相关联的table样式,加上word-break:break-all这样的解药。

总结四:Select(下拉弹框类)组件页面滚动时,下拉内容与弹出父组件分离

image
如上图所示,外层页面一滚动,下拉框与下拉内容就分离了,分离,离,离了。这个出现原就是因为ANTD所有的弹框都是默认挂载在body下面,然后绝对定位的。所以滚动body内容, 就会造成与弹出触发的那个组件错位。幸好在3.0以后ANTD对这个bug做出了一个解决方案,就是增加了getPopupContainer属性,可以让下拉内容挂载任何存在的dom节点上,并相对其定位。具体用例请查看官方示例

Hooks自定义组件报cannot be given refs

Warning: Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()

随着React hooks不断的应用,在antd中,用hooks自定义的组件,会报如上所示的refs无法获取,这是antd form双向绑定对ref有需要。因为ref和key一样,不是通过prop来传递的,react对其有特殊的处理。好在这个可以通过forwardRef来解决,官方文档,看一个示例:

export default function FundSelect(props) {
    return (
        <YourComponent {...props}>
    )
}

使用forwardRef转发Ref

function FundSelect(props, ref) {
    return (
        <YourComponent ref={ref} {...props}>
    )
}

export default forwardRef(FundSelect);

getFieldDecorator包裹下的Switch组件无法显示为true状态

<FormItem {...formItemLayout} label="是否显示">
     {getFieldDecorator('enable', {
      initialValue: true,})
(<Switch />)}
 </FormItem>

发现在getFieldDecorator包裹下的Switch组件无法显示为true的状态,3.23版本报以下提示信息:

warning.js:6 Warning: [antd: Switch] value is not validate prop, do you mean checked?

查找文档得知Switch组件是通过checked的属性来显示状态的,所以需要一个额外的属性valuePropName

<FormItem {...formItemLayout} label="是否显示文档">
            {getFieldDecorator('showDocument', {
              initialValue: true,
              valuePropName: 'checked'
            })(<Switch />)}
</FormItem>

文章首发于: http://closertb.site

(https://github.com/closertb/c...


前端黑洞
3.5k 声望4.7k 粉丝

不要假装很努力,因为结果不会陪你演戏


引用和评论

0 条评论