美文网首页React
Ant Design Pro 使用之 表单组件再封装

Ant Design Pro 使用之 表单组件再封装

作者: anyesu | 来源:发表于2019-02-16 21:18 被阅读24次

    背景


    使用 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)".

    解决办法就是使用 reduxProvider 组件包裹一下

    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 版


    转载请注明出处:https://www.jianshu.com/p/c7120bf2e4f8

    相关文章

      网友评论

        本文标题:Ant Design Pro 使用之 表单组件再封装

        本文链接:https://www.haomeiwen.com/subject/fjnieqtx.html