背景
使用 Ant Design Pro 开发有一段时间了,表单作为后台系统常见的功能当然很有必要封装一下,减少代码重复量。虽说 antd 的表单组件已经很不错了,但是使用上还是太麻烦了( 我就是懒 ),所以我就基于一些小小的约定封装了它的上层业务组件,更方便调用:
- 常用表单场景主要分四类:搜索条件、详情页、弹出式窗口、其他混合型
- 表单布局主要分三类:水平排列、垂直排列、复杂混合型
- 弹窗类型分两类:模态对话框、屏幕边缘滑出的浮层面板 ( 抽屉 )
- 封装尽可能不引入新的语法,兼容 antd 原有配置方式
- 调用尽可能简单,减少重复关键字的使用。( 比如:
getFieldDecorator
)
基础表单组件
-
组件定义
import React, { Component } from 'react'; import { Form } from 'antd'; import PropTypes from 'prop-types'; import { renderFormItem, fillFormItems, submitForm } from './extra'; const defaultFormLayout = { labelCol: { span: 5 }, wrapperCol: { span: 15 } }; /** * 基础表单 */ @Form.create({ // 表单项变化时调用 onValuesChange({ onValuesChange, ...restProps }, changedValues, allValues) { if (onValuesChange) onValuesChange(restProps, changedValues, allValues); }, }) class BaseForm extends Component { static propTypes = { layout: PropTypes.string, formLayout: PropTypes.object, hideRequiredMark: PropTypes.bool, dataSource: PropTypes.array, formValues: PropTypes.object, renderItem: PropTypes.func, onSubmit: PropTypes.func, // eslint-disable-next-line react/no-unused-prop-types onValuesChange: PropTypes.func, }; static defaultProps = { layout: 'horizontal', formLayout: undefined, hideRequiredMark: false, dataSource: [], formValues: {}, renderItem: renderFormItem, onSubmit: () => {}, onValuesChange: undefined, }; /** * 表单提交时触发 * * @param e */ onSubmit = e => { if (e) e.preventDefault(); // 阻止默认行为 this.submit(); }; /** * 调用表单提交 */ submit = () => { const { form, formValues, onSubmit } = this.props; submitForm(form, formValues, onSubmit); }; render() { const { children, layout, formLayout = layout === 'vertical' ? null : defaultFormLayout, hideRequiredMark, renderItem, form: { getFieldDecorator }, formValues, dataSource, } = this.props; return ( <Form layout={layout} onSubmit={this.onSubmit} hideRequiredMark={hideRequiredMark}> {children || fillFormItems(dataSource, formValues).map(item => renderItem(item, getFieldDecorator, formLayout) )} </Form> ); } } export * from './extra'; export default BaseForm;
-
调用示例
<BaseForm hideRequiredMark={false} layout="vertical" formLayout={null} dataSource={[ { label: 'key1', name: 'name1', required: true }, { label: 'key2', name: 'name2', required: true }, { label: 'key3', name: 'name3' }, ]} formValues={{ name2: 'default' }} onSubmit={() => {}} onValuesChange={() => {}} wrappedComponentRef={form => { this.form = form; }} />
比起 antd 表单组件的调用应该简洁不少吧
弹出式表单组件
-
组件定义
import React, { PureComponent } from 'react'; import ReactDOM from 'react-dom'; import PropTypes from 'prop-types'; import BaseComponent from '../BaseComponent'; import BaseForm from '../BaseForm'; const destroyFns = []; // 保存所有弹框的引用 /** * 弹出式表单 */ class PopupForm extends PureComponent { static propTypes = { layout: PropTypes.string, formLayout: PropTypes.object, hideRequiredMark: PropTypes.bool, width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), title: PropTypes.string, root: PropTypes.object, okText: PropTypes.string, cancelText: PropTypes.string, onValuesChange: PropTypes.func, closeOnSubmit: PropTypes.bool, onClose: PropTypes.func, }; static defaultProps = { layout: 'vertical', formLayout: null, hideRequiredMark: false, width: 720, title: undefined, root: undefined, okText: '确定', cancelText: '取消', onValuesChange: undefined, closeOnSubmit: true, onClose: undefined, }; /** * 显示通过getInstance创建的组件 * * @param formValues 表单初始值 */ static show(formValues) { const { instance } = this; if (instance) { const { root } = instance.props; if (root instanceof BaseComponent) { root.showPopup(this.name, true, formValues); } } } /** * 创建一个该类型表单组件的实例,配合show显示/关闭 * * @param root 表单组件引用的父组件,用于统一管理表单组件的状态 * @param props 组件属性 * @returns {*} */ static getInstance(root, props) { if (root instanceof BaseComponent) { const { forms = {} } = root.state || {}; const form = forms[this.getFormName()] || {}; this.instance = <this root={root} {...form} {...props} />; return this.instance; } return null; } /** * 接口方式创建并显示一个表单组件,独立于App容器之外 * * @param props 组件属性 * @param decorators 要给组件附加的高阶组件 * @returns {*} */ static open(props, decorators) { const Com = decorators ? [].concat(decorators).reduce((pre, item) => item(pre), this) : this; const div = document.createElement('div'); const close = () => { const unmountResult = ReactDOM.unmountComponentAtNode(div); if (unmountResult && div.parentNode) { div.parentNode.removeChild(div); } const pos = destroyFns.findIndex(item => item === close); if (pos >= 0) destroyFns.splice(pos, 1); }; // 使用DvaContainer作为新的根组件,保证子组件正常使用redux const rootContainer = window.g_plugins.apply('rootContainer', { initialValue: <Com {...props} visible onClose={close} />, }); ReactDOM.render(rootContainer, div); destroyFns.push(close); // 返回一个对象,通过这个对象来显式关闭组件 return { close }; } /** * 销毁全部弹框 */ static destroyAll() { while (destroyFns.length) { const close = destroyFns.pop(); if (close) close(); } } /** * 获取表单名称,用于父组件对表单组件的控制,默认取组件类名 * * @returns {string} */ static getFormName() { return this.name; } /** * 表单提交时触发 * * @param fieldsValue * @param form */ onSubmit = (fieldsValue, form) => { const { onSubmit, closeOnSubmit = false } = this.props; if (closeOnSubmit === true) { // 表单提交时关闭当前组件 this.close(); } onSubmit(fieldsValue, form); }; /** * 点击Ok按钮时触发 * * @param e */ onOk = e => { if (e) e.preventDefault(); // 阻止默认行为 const { form: { submit } = {} } = this; if (submit) { // 通过子组件暴露的方法,显示提交表单 submit(); } }; /** * 点击Cancel按钮时触发 * * @param e */ onCancel = e => { if (e) e.preventDefault(); // 阻止默认行为 this.close(); }; /** * 关闭当前组件 */ close = () => { const { onClose, root } = this.props; const formName = this.constructor.getFormName(); if (onClose) { onClose(formName); } else if (root instanceof BaseComponent) { // 对应getInstance创建的组件,由父组件控制 root.showPopup(formName, false); } }; /** * 绘制表单,可覆盖 * * @returns {*} */ renderForm = () => { const { children, layout, formLayout, hideRequiredMark, onValuesChange, formValues, ...restProps } = this.props; return ( <BaseForm {...restProps} hideRequiredMark={hideRequiredMark} layout={layout} formLayout={formLayout} dataSource={this.getDataSource()} formValues={formValues} onSubmit={this.onSubmit} onValuesChange={onValuesChange} wrappedComponentRef={form => { this.form = form; }} /> ); }; /** * 绘制组件主体内容,可覆盖 * * @returns {PopupForm.props.children | *} */ renderBody = () => { const { children } = this.props; return children || this.renderForm(); }; /** * 表单字段数据源,可覆盖 * * @returns {undefined} */ getDataSource = () => undefined; /** * 组件显示标题,可覆盖 * * @returns {string} */ getTitle = () => ''; } export default PopupForm;
-
这个是基础组件,不能直接使用,具体的弹框表现形式由子类实现,主要为 模态框 和 抽屉
-
调用方式和常规组件不一样,采用继承的方式实现具体的业务组件,通过组件的静态方法实现渲染和行为控制 ( 当然要使用 JSX 也是可以的)
-
API
方法 说明 getInstance
创建一个该类型表单组件的实例,配合 show
显示 / 关闭show
显示通过 getInstance
创建的组件弹框open
接口方式创建并显示一个表单组件,独立于 App 容器之外。<br />返回一个对象,通过这个对象引用来显式关闭组件 destroyAll
销毁所有通过 open
创建的组件弹框
模态框式表单组件
-
组件定义
import React from 'react'; import { Modal } from 'antd'; import PropTypes from 'prop-types'; import PopupForm from '../PopupForm'; /** * 模态框式表单 */ class ModalForm extends PopupForm { static propTypes = { layout: PropTypes.string, formLayout: PropTypes.object, hideRequiredMark: PropTypes.bool, width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), title: PropTypes.string, root: PropTypes.object, okText: PropTypes.string, cancelText: PropTypes.string, onValuesChange: PropTypes.func, closeOnSubmit: PropTypes.bool, }; static defaultProps = { layout: 'horizontal', formLayout: undefined, hideRequiredMark: false, width: 640, title: undefined, root: undefined, okText: '确定', cancelText: '取消', onValuesChange: undefined, closeOnSubmit: true, }; render() { const { children, title, width, visible, okText, cancelText, ...restProps } = this.props; return visible ? ( <Modal title={title || this.getTitle()} width={width} visible okText={okText} onOk={this.onOk} cancelText={cancelText} onCancel={this.onCancel} {...restProps} destroyOnClose > {this.renderBody()} </Modal> ) : null; } } export default ModalForm;
-
调用示例
class Demo1 extends ModalForm { getTitle = () => '模态框式表单'; getDataSource = () => [ { label: 'key1', name: 'name1', required: true }, { label: 'key2', name: 'name2', required: true }, { label: 'key3', name: 'name3' }, ]; } <Button type="primary" onClick={() => Demo1.open({ title: '覆盖表单标题' })}> 新增 </Button>
抽屉式表单组件
-
组件定义
import React from 'react'; import { Drawer, Button } from 'antd'; import PropTypes from 'prop-types'; import PopupForm from '../PopupForm'; /** * 抽屉式表单 */ class DrawerForm extends PopupForm { static propTypes = { layout: PropTypes.string, formLayout: PropTypes.object, hideRequiredMark: PropTypes.bool, width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), title: PropTypes.string, root: PropTypes.object, okText: PropTypes.string, cancelText: PropTypes.string, onValuesChange: PropTypes.func, closeOnSubmit: PropTypes.bool, closable: PropTypes.bool, }; static defaultProps = { layout: 'vertical', formLayout: null, hideRequiredMark: false, width: 720, title: undefined, root: undefined, okText: '确定', cancelText: '取消', onValuesChange: undefined, closeOnSubmit: false, closable: false, }; /** * 绘制组件按钮 * * @returns {*} */ renderFooter = () => { const { okText, cancelText } = this.props; return ( <div style={{ position: 'absolute', left: 0, bottom: 0, width: '100%', borderTop: '1px solid #e9e9e9', padding: '10px 16px', background: '#fff', textAlign: 'right', }} > {cancelText ? ( <Button onClick={this.onCancel} style={{ marginRight: 8 }}> {cancelText} </Button> ) : null} {okText ? ( <Button onClick={this.onOk} type="primary"> {okText} </Button> ) : null} </div> ); }; render() { const { children, title, width, visible, closable, formLayout, ...restProps } = this.props; return visible ? ( <Drawer title={title || this.getTitle()} width={width} visible closable={closable} onClose={this.onCancel} {...restProps} destroyOnClose > <div style={{ paddingBottom: 75 }}>{this.renderBody()}</div> {this.renderFooter()} </Drawer> ) : null; } } export default DrawerForm;
-
调用示例
class Demo1 extends DrawerForm { getTitle = () => '模态框式表单'; getDataSource = () => [ { label: 'key1', name: 'name1', required: true }, { label: 'key2', name: 'name2', required: true }, { label: 'key3', name: 'name3' }, ]; } <Button type="primary" onClick={() => Demo1.open({ title: '覆盖表单标题' })}> 新增 </Button>
搜索表单组件
-
组件定义
import React, { Component } from 'react'; import { Form } from 'antd'; import PropTypes from 'prop-types'; import { submitForm } from '../BaseForm'; /** * 搜索表单 */ @Form.create({ // 表单项变化时调用 onValuesChange({ onValuesChange, ...restProps }, changedValues, allValues) { if (onValuesChange) onValuesChange(restProps, changedValues, allValues); }, }) class SearchForm extends Component { static propTypes = { root: PropTypes.object, onSearch: PropTypes.func, layout: PropTypes.string, render: PropTypes.func, }; static defaultProps = { root: undefined, onSearch: undefined, layout: 'inline', render: undefined, }; constructor(props) { super(props); const { root } = this.props; if (root) root.searchForm = this; } /** * 调用搜索 * * @param formValues */ search = formValues => { const { onSearch } = this.props; if (onSearch) onSearch(formValues); }; /** * 重置表单并搜索 */ reset = (searchOnReset = true) => { const { form, formValues } = this.props; form.resetFields(); if (searchOnReset === true) this.search(formValues); }; /** * 表单提交时触发 * * @param e */ onSubmit = e => { if (e) e.preventDefault(); const { form, formValues } = this.props; submitForm(form, formValues, this.search); }; render() { const { render, hideRequiredMark, layout } = this.props; return ( <Form hideRequiredMark={hideRequiredMark} layout={layout} onSubmit={this.onSubmit}> {render ? render(this.props) : null} </Form> ); } } export default SearchForm;
-
调用示例
import React, { Component, Fragment } from 'react'; import { Form, Button, Col, Input, Row, message } from 'antd'; import SearchForm from '@/components/SearchForm'; import { renderFormItem } from '@/components/BaseForm'; export default class Demo extends Component { search = data => message.success(`搜索提交:${JSON.stringify(data)}`); renderSearchForm = ({ form: { getFieldDecorator } }) => ( <Fragment> <Row> <Button icon="plus" type="primary"> 新增 </Button> </Row> <Row style={{ marginTop: 16 }}> <Col span={18}> <Form.Item label="条件1"> {getFieldDecorator('param1')(<Input placeholder="请输入" />)} </Form.Item> {renderFormItem({ label: '条件2', name: 'param2' }, getFieldDecorator)} {renderFormItem({ label: '条件3', name: 'param3' }, getFieldDecorator)} </Col> <Col span={6} style={{ textAlign: 'right' }}> <span> <Button type="primary" htmlType="submit"> 查询 </Button> <Button style={{ marginLeft: 8 }} onClick={() => this.searchForm.reset()}> 重置并提交 </Button> <Button style={{ marginLeft: 8 }} onClick={() => this.searchForm.reset(false)}> 只重置 </Button> </span> </Col> </Row> </Fragment> ); render() { return ( <SearchForm root={this} onSearch={this.search} render={this.renderSearchForm} searchOnReset={false} /> ); } }
遇到的问题
在实际使用的过程中,弹框表单的子组件中可能会包含被 connect 的组件,光使用 antd 的弹框组件包裹就会报错:
Uncaught Error: Could not find "store" in either the context or props of "Connect(Demo)". Either wrap the root component in a <Provider>, or explicitly pass "store" as a prop to "Connect(Demo)".
解决办法就是使用 redux 的 Provider 组件包裹一下
ReactDOM.render(
// 使用 Provider 使子组件能从上下文中访问 store
// 注意react-redux版本要和dva引用的版本一致,否则子组件使用@connect会出错
// eslint-disable-next-line no-underscore-dangle
<Provider store={window.g_app._store}>
<Com {...props} visible onClose={close} />
</Provider>,
div
);
具体调用位置在上面 PopupForm.open
中,该代码已经按 dva 提供的方式进行解决了。
最后
完整代码已经传到 CodeSandbox ,点击查看antd 版 或者 antd pro 版
网友评论