Antd form表单校验失败滚动问题

1,469 阅读4分钟

Antd form表单校验失败滚动问题

在业务代码中发现被form表单包裹的高阶组件无法在校验规则失败后滚动到视图中

form3 validateFieldsAndScroll

validateFieldsAndScroll 会调用validateFields 校验表单 存在校验不通过调用antd提供的回调。回调函数会先从filedStore获取所有需要校验的表单域名称字段,循环是否存在错误错误项,存在错误项则调用获取getFieldInstance获取实例instance,判断instance是否存在,存在则通过ReactDom提供的findDomNode找到对应的元素节点,最终通过scrollIntoView滚动到视图中。

而失效的原因是因为通过getFieldInstance获取实例为undefined

image-20221130110725785.png getFieldInstance的定义

getFieldInstance: function getFieldInstance(name) {
        return this.instances[name];
}

this.instance在哪里注册进去的呢?回头想想form3注册表单的用法,是通过getFieldDecorator注册表单域的,可能大概率是在这里注册的

getFiedleDecorator定义,返回一个包裹的高阶组件,主要做的事情是获取初识表单域的原数据fieldMeta,并赋值props和ref

getFieldDecorator: function getFieldDecorator(name, fieldOption) {
        var _this2 = this;
​
        var props = this.getFieldProps(name, fieldOption);
        return function (fieldElem) {
          // We should put field in record if it is rendered
          _this2.renderFields[name] = true;
​
          var fieldMeta = _this2.fieldsStore.getFieldMeta(name);
          var originalProps = fieldElem.props;
          if (process.env.NODE_ENV !== 'production') {
            var valuePropName = fieldMeta.valuePropName;
            warning(!(valuePropName in originalProps), '`getFieldDecorator` will override `' + valuePropName + '`, ' + ('so please don't set `' + valuePropName + '` directly ') + 'and use `setFieldsValue` to set it.');
            var defaultValuePropName = 'default' + valuePropName[0].toUpperCase() + valuePropName.slice(1);
            warning(!(defaultValuePropName in originalProps), '`' + defaultValuePropName + '` is invalid ' + ('for `getFieldDecorator` will set `' + valuePropName + '`,') + ' please use `option.initialValue` instead.');
          }
          fieldMeta.originalProps = originalProps;
          fieldMeta.ref = fieldElem.ref;
          var decoratedFieldElem = React.cloneElement(fieldElem, _extends({}, props, _this2.fieldsStore.getFieldValuePropValue(fieldMeta)));
          return supportRef(fieldElem) ? decoratedFieldElem : React.createElement(
            FieldElemWrapper,
            { name: name, form: _this2 },
            decoratedFieldElem
          );
        };
      }

可以看到返回的的高阶组件中部分props属性是来源于getFieldProps,断点进去看看其定义,可以看到这里有一个关于ref的赋值,关注到saveRef函数定义中this.instances[name] = component,可以清楚地认识到getFieldInstance拿到的instance就是component

var inputProps = _extends({}, this.fieldsStore.getFieldValuePropValue(fieldOption), {
          ref: this.getCacheBind(name, name + '__ref', this.saveRef)
});
saveRef: function saveRef(name, _, component) {
    if (!component) {
      var _fieldMeta = this.fieldsStore.getFieldMeta(name);
      if (!_fieldMeta.preserve) {
        // after destroy, delete data
        this.clearedFieldMetaCache[name] = {
          field: this.fieldsStore.getField(name),
          meta: _fieldMeta
        };
        this.clearField(name);
      }
      delete this.domFields[name];
      return;
    }
    this.domFields[name] = true;
    this.recoverClearedField(name);
    var fieldMeta = this.fieldsStore.getFieldMeta(name);
    if (fieldMeta) {
      var ref = fieldMeta.ref;
      if (ref) {
        if (typeof ref === 'string') {
          throw new Error('can not set ref string for ' + name);
        } else if (typeof ref === 'function') {
          ref(component);
        } else if (Object.prototype.hasOwnProperty.call(ref, 'current')) {
          ref.current = component;
        }
      }
    }
    this.instances[name] = component;
  }

那这个component是哪里来的呢。我们知道refreact提供的一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素,关于ref的回调官网说明

如果 ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的

这个component就是ref 绑定的那个DOM元素,所以在validateFieldsAndScroll拿到的instance可以通过React.findDomNode获取到对应的实例节点,从而将其移动到校验失败的表单项。

移动失败是因为this.instances拿不到对应的实例,在saveRef中是判断了如果component不存在则不会在instance注入。那为啥会返回空呢?回到我们对form表单项的用法,这里用到了高阶组件,等等,发现事情不对劲,高阶组件的ref不会传递下去,也就是在这里ref丢失了

FormComponent: (
                <ActName disabled={isDisabled('name', type)} placeholder={`请输入名称`} allowClear />
                
const ActName = props => {
    return (
        <>
            <MyInput.Def {...props} />
            <p className="warm-prompt">some text</p>
        </>
    );
}

通过forwardRef获取ref并将其绑定在对应的dom元素中,验证通过✌️

这里发现了一个小小的问题,看getFieldDecorator中的这段代码

var decoratedFieldElem = React.cloneElement(fieldElem, _extends({}, props, _this2.fieldsStore.getFieldValuePropValue(fieldMeta)));

getFieldValuePropValue是获取固定的'value'值,除非meta中传了getValueProps。但这个antd没有暴露出可以传这个配置项,也就是说antd 在创建高阶组件时会将我们传递的ref给覆盖掉,虽然有这段代码~

fieldMeta.ref = fieldElem.ref;

form4scrollToFirstErrorscrollToField 失效和遗忘的submit

form4提供了scrollToFirstError属性是否当校验不通过时滚动到第一个校验失败的元素,我开启了然后呢form包裹中添加下面的元素或者通过form.submit方法触发,scrollToFirstError才生效

<Button type="primary" htmlType="submit">
          Submit
  </Button>

关于scrollToFirstErrorscrollToField 失效的原因其实在ant的官方文档有说明

滚动依赖于表单控件元素上绑定的 id 字段,如果自定义控件没有将 id 赋到正确的元素上,这个功能将失效

form4相较于form3的部分改动是在于滚动元素不再依赖于ref,而是通过给元素绑定id,通过document.getElementById获取到对应的元素,所以在高阶组件中为啥要传递id的原因

scrollToField: function scrollToField(name) {
        var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
        var namePath = (0, _util.toArray)(name);
        var fieldId = (0, _util.getFieldId)(namePath, wrapForm.__INTERNAL__.name);
        var node = fieldId ? document.getElementById(fieldId) : null;
​
        if (node) {
          (0, _scrollIntoViewIfNeeded["default"])(node, (0, _extends2["default"])({
            scrollMode: 'if-needed',
            block: 'nearest'
          }, options));
        }
      }