Antd form表单校验失败滚动问题
在业务代码中发现被form表单包裹的高阶组件无法在校验规则失败后滚动到视图中
form3 validateFieldsAndScroll
validateFieldsAndScroll 会调用validateFields 校验表单 存在校验不通过调用antd提供的回调。回调函数会先从filedStore获取所有需要校验的表单域名称字段,循环是否存在错误错误项,存在错误项则调用获取getFieldInstance获取实例instance,判断instance是否存在,存在则通过ReactDom提供的findDomNode找到对应的元素节点,最终通过scrollIntoView滚动到视图中。
而失效的原因是因为通过getFieldInstance获取实例为undefined
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是哪里来的呢。我们知道ref是react提供的一种方式,允许我们访问 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;
form4scrollToFirstError 和 scrollToField 失效和遗忘的submit
form4提供了scrollToFirstError属性是否当校验不通过时滚动到第一个校验失败的元素,我开启了然后呢form包裹中添加下面的元素或者通过form.submit方法触发,scrollToFirstError才生效
<Button type="primary" htmlType="submit">
Submit
</Button>
关于scrollToFirstError 和 scrollToField 失效的原因其实在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));
}
}