前言
表单是我们日常操作中接触最多的一个组件,ant-design默认提供的组件在表单验证,表单生成方面需要编写太多的重复代码,令人生厌,故在此将其进行二次封装,方便调用以提高开发效率。
我们期望能够拥有以下特性:
- 配置式表单,表单项可根据配置生成
- 表单项支持数据填充
- 表单内容验证
- 表单支持收缩展开(如果没有字段是展开可见的,则不显示展开收缩菜单)
- 在以上情况无法满足需求时,支持自定义注入表单内容
组件调用示例1——配置式表单
const formItems = [{
label: 'id',
name: 'id',
itemRender: <Input type="hidden" />,
hidden: true,
},
{
label: '首页链接',
name: 'homePage',
// 验证规则,详见ant-design的表单组件
rules: [
{
required: true,
message: '首页链接不能为空',
},
{
type: 'url',
message: '请输入正确的链接地址!',
},
],
itemRender: <Input placeholder="首页链接" />,
},
{
label: '名称',
name: 'name',
// 默认值
defaultValue: '简书',
rules: [
{
required: true,
message: '站点名称不能为空',
},
],
itemRender: <Input placeholder="请输入站点全称" />,
},
{
label: '超时时间'
name: 'timeOut',
itemRender: <Input placeholder="超时时间,单位s(秒)" />,
// 标注toggleField默认隐藏
toggleField: true,
},
}];
const formValues = {
homePage: 'test',
timeOut: 3,
};
<BaseForm
formItems={formItems}
formValues={formValues}
// onValuesChange={onValuesChange}
// 表单验证通过后触发的提交操作
// onSubmit={onSubmit}
wrappedComponentRef={v => {
this.form = v;
}}
/>
组件调用示例2——自定义注入表单内容
class TestForm extends PureComponent {
render() {
const {
form: { getFieldDecorator },
record,
} = this.props;
return (
<Fragment>
{getFieldDecorator('id', {
initialValue: record.id,
})(<Input type="hidden" />)}
<Form.Item labelCol={{ span: 5 }} wrapperCol={{ span: 15 }} label="区域名称">
{getFieldDecorator('name', {
initialValue: record.name,
rules: [
{
required: true,
message: '区域名称不能为空',
},
],
})(<Input placeholder="区域名称" />)}
</Form.Item>
</Fragment>
);
}
}
<BaseForm
wrappedComponentRef={v => {
this.form = v;
}}
>
<TestForm />
</BaseForm>
重点讲解
- 定义state属性
toggleFieldVisibility
用来控制表单的展开折叠。可以想到,我们点击展开折叠按钮时,通过改变这个字段的状态来控制折叠表单项的显示和隐藏。
- 为了支持
自定义表单项
和配置式
两种渲染方式,我们在render
函数中做了个判断,判断用户如果传入children(自定义表单项)
时则渲染children
,否则调用自定义的渲染函数根据表单项配置
进行渲染。
- 用户如果使用
children(自定义表单项)
,我们需要为这些表单项输入form
属性。这里我们通过React.cloneElement
实现属性注入。
- 对于
配置式表单项
,我们通过逐个渲染Form.Item
来实现,并根据预定义的配置来注入一些属性,如显示/隐藏
(组合toggleFieldVisibility
和表单项的hidden
属性来控制显隐),数据填充
(判断表单项是否有值-formValues
,没有话则注入默认值defaultValue
)
- 我们提供一个
submit
函数用来在父组件中进行显式调用表单提交。这个提交操作包装了表单验证的操作,同时用户可以可以传入真实表单提交函数以在验证成功后触发自定义的操作。(这里可在父组件里通过wrappedComponentRef
来引用BaseForm
并调用submit
操作)
组件定义
import React, { Component, Fragment } from 'react';
import { Form, Icon } from 'antd';
import { renderFormItems, submitForm } from '../utils';
@Form.create({
// 表单项变化时调用,传入onValuesChange进行回调
onValuesChange({ onValuesChange, ...restProps }, changedValues, allValues) {
if (onValuesChange) onValuesChange(restProps, changedValues, allValues);
},
})
class BaseForm extends Component {
static defaultProps = {
layout: 'horizontal',
formValues: {},
};
constructor(props) {
super(props);
this.state = {
/**
* 表单折叠项可见性
*/
toggleFieldVisibility: false,
};
}
/**
* 表单提交时触发
*
* @param e
*/
onSubmit = e => {
if (e) e.preventDefault();
this.submit(e);
};
/**
* 调用表单提交
* @param e
* @param extraOptions 其他组件传递进来的额外参数
*/
submit = (e, extraOptions) => {
const { form, formValues, onSubmit } = this.props;
submitForm(form, formValues, onSubmit, extraOptions);
};
/**
* 收缩、展开表单
*/
toggleForm = () => {
const { toggleFieldVisibility } = this.state;
this.setState({
toggleFieldVisibility: !toggleFieldVisibility,
});
};
/**
* 默认表单主体渲染器(根据配置生成)
* @returns {*}
*/
renderFormBody = () => {
const {
form: { getFieldDecorator },
formItems,
formValues,
} = this.props;
const { toggleFieldVisibility } = this.state;
return (
<Fragment>
{renderFormItems(formItems, getFieldDecorator, formValues, toggleFieldVisibility)}
{formItems.some(item => item.toggleField) && (
<div style={{ textAlign: 'center' }} onClick={this.toggleForm}>
<a style={{ marginLeft: 8 }}>
{(toggleFieldVisibility && '收起') || '更多'}
{(toggleFieldVisibility && <Icon type="up" />) || <Icon type="down" />}
</a>
</div>
)}
</Fragment>
);
};
render() {
const { children, layout, form, formValues } = this.props;
return (
<Form layout={layout} onSubmit={this.onSubmit}>
{
// 自定义表单内容,并且注入表单相关属性
(children &&
React.Children.map(children, child =>
React.cloneElement(child, { form, record: formValues }),
)) ||
this.renderFormBody()}
</Form>
);
}
}
export default BaseForm;
import React from 'react';
import { Form } from 'antd';
import { isFunction } from 'lodash';
const defaultFormLayout = {
labelCol: { span: 7 },
wrapperCol: { span: 13 },
};
const fetchFieldDecorator = (item, getFieldDecorator) => {
const { name, defaultValue, rules, itemRender, fieldDecoratorProps } = item;
return getFieldDecorator(name, {
initialValue: defaultValue,
rules,
...fieldDecoratorProps,
})(itemRender);
};
/**
* 根据配置渲染单个表单项
*
* @param item
* @param getFieldDecorator
* @returns {*}
*/
export const renderFormItem = (item, getFieldDecorator) => {
const { name, label, formItemLayout, style, formItemProps } = item;
return (
<Form.Item key={name} label={label} {...formItemLayout} style={style} {...formItemProps}>
{fetchFieldDecorator(item, getFieldDecorator)}
</Form.Item>
);
};
/**
* 根据配置渲染所有的表单项
* @param items
* @param getFieldDecorator
* @param formValues
* @param toggleFieldVisibility
* @param layout
* @return
*/
export const renderFormItems = (
items,
getFieldDecorator,
formValues = {},
toggleFieldVisibility,
layout
) =>
items.map(item => {
const { style, defaultValue, hidden, ...restProps } = item;
const display =
((hidden === true || (item.toggleField && toggleFieldVisibility === false)) && 'none') ||
'block';
let defaultVal = defaultValue;
if (formValues[item.name] !== undefined) {
defaultVal = formValues[item.name];
}
return renderFormItem(
{
formItemLayout: layout === 'vertical' ? null : defaultFormLayout,
...restProps,
style: { ...style, display },
defaultValue: defaultVal,
},
getFieldDecorator
);
});
/**
* submit form
* @param form
* @param formValues
* @param callback
* @param extraOptions
*/
export const submitForm = (form, formValues, callback, extraOptions) => {
if (form) {
form.validateFieldsAndScroll((err, fieldsValue) => {
if (!err && isFunction(callback)) {
callback({ ...formValues, ...fieldsValue }, form, extraOptions);
}
});
} else {
// eslint-disable-next-line no-console
console.warn('form is not defined');
}
};
结语
希望本文能给您以启发,欢迎留言交流讨论。