本文作者:IMWeb chenxd1996 原文出处:IMWeb社区 未经同意,禁止转载
最近笔者在用React+antd做管理后台系统需求的时候,碰到了一个问题,就是在同一个antd的FormItem下面有多个子数据,那么在表单校验的时候某个数据一旦出错,整个FormItem下面的表单组件都会标红,无法准确标出出错的字段。
如图所示,这里的表格数据,其实都是同一个数据字段的子字段。可以看到,即使只有第一个input框校验出错,也会出现一个大红框,出错信息也是显示在整个表格下方,很难看到具体出错的位置。
我们的目标效果应该是这样的:
Form表单下面是不能嵌套Form表单的,所以笔者试着自己写了一个简单的表单校验器。虽然有点简陋,但感觉也还有点意思,与大家分享一下。
首先能想到的是模仿getFieldDecorator,提供一个函数getField,调用getField(option)(formComponent)得到一个包装过的Component,在原来表单组件上加入错误信息显示。
例如:
getField({
field: 'name', // field相当于是该字段的id,支持类似'userInfo.name', 'users[0].userInfo.name'
validator: (value, values, callback) {
// value为该字段的值
// values为该字段的父字段的值。如果value是userInfo.name的值, 那么values就是userInfo
// callback()时候为校验成功,callback('some error msg') 为检验失败
}
})
好了,上面就是实现目标了,接下来开始一步步实现。
首先,肯定是要有一个容器用来存放校验器的,getField这个方法就是为了存放校验器,这个容器还要暴露出一个validate方法,这个方法一旦被调用,所有的校验器就都被调用,如果出错就会显示错误信息。
这个容器可以用class来实现,其大概内容应该如下:
class MyValidation {
toValidate = {}; // 用来存放校验器的,key是field,value是validator
constructor(context) {
// context是表单所在Component,用来更新视图用的
this.context = context
}
add(field, validator) { // 用来添加校验器的
}
@autobind
getField(options = {}) {
// 调用add函数保存校验器,并返回一个包装过element
// 包装element也叫高阶组件,目的为了在原有组件下面显示出错信息
}
@autobind
validate(values) {
// 用来触发校验,values是表单数据
}
@autobind
clearFields() {
// 用来清除校验信息
}
}
看到这里,聪明的你也许已经看穿了一切了,如果有兴趣,可以照着这个思路,自己把细节实现一下。
接下来,我们将继续将探究一下每个函数的实现细节。
add
这个函数非常简单,如下:
add(field, validator) { // 用来添加校验器的
this.toValidate[field] = {
validator,
};
}
getField
这个函数略有些复杂,需要对React高阶组件有一定的了解
getField(options = {}) {
// 调用add函数保存校验器,并返回一个包装过element
// 包装element也叫高阶组件,目的为了在原有组件下面显示出错信息
const { field, validator } = options;
this.add(field, validator); // 保存校验器
return (Cmp) => { // Cmp就是将被包装的表单组件,例如<Input />
return function (props) { // 函数式组件,因为不需要state
Cmp.props = { ...Cmp.props, ...props }; // 将props传给Cmp
const msg = errorRecord[field]; // errorRecord是全局变量,存放错误信息,看一下是否有错误信息
return (
<div className={msg ? 'has-error' : null}>
{Cmp}
{
<div className="ant-form-explain" style={errorMsgStyle}>
<span>{msg}</span>
</div>
}
</div>
);
};
};
}
这里需要说明的是,errorRecord是个全局变量,这里不是通过高阶组件的setState来更新视图的,后面会讲到校验后如何触发视图更新。这里我没有自己写的样式,是直接用的antd表单校验的样式。
validate
validate(values) {
// 用来触发校验,values是表单数据
// context是表单所在Component,用来更新视图用的
const fields = Object.keys(this.toValidate);
let isValid = true;
fields.forEach((field) => {
const { validator } = this.toValidate[field];
const value = _.get(values, field); // 这里的_是lodash库,用它的get方法,获取对象的值,非常强大
const callbackCreator = (field) => {
return (msg) => { // 这里用了一个闭包,生成field的对应callback
errorRecord[field] = msg;
};
};
if (validator) {
validator(value, values, callbackCreator(field));
}
const msg = errorRecord[field];
if (msg) { // 如果有msg,说明出错了
isValid = false;
}
});
this.context.forceUpdate(); // 这里要更新下视图
return isValid;
}
clearFields
clearFields() {
// 用来清除校验信息
errorRecord = {};
this.context.forceUpdate();
}
大工告成,接下来看看如何使用吧,使用方法大概如下:
const validatation = new myValidation(this);
const { getField } = validation;
function () {
}
class TestCmp extends React.Component {
getFields = () => {
const Wrapper = getField({
field: 'name',
validator: (value, values, callback) => {
if (!value || value === '') {
callback('名字不能为空');
return;
}
callback();
}
})(<Input />);
return <Wrapper />
}
submit = () => {
const isValid = validation.validate(this.props.form.getFieldsValue());
if (!isValid) {
return;
}
// submit form data
}
render() {
return (
<Form>
<FormItem>
{
this.getFields();
}
</FormItem>
</Form>
<Button onClick={this.submit}>
)
}
}
有时候简单的代码写多了感觉会比较平淡,遇到一些稍有难度问题,反而能让你学到更多的东西。笔者学习React不足两个月,可能在很多方面还是理解得不够到位,如有纰漏,欢迎读者批评指正,谢谢!