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

背景


使用 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 (
          
    {children || fillFormItems(dataSource, formValues).map(item => renderItem(item, getFieldDecorator, formLayout) )}
    ); } } export * from './extra'; export default BaseForm;
  • 调用示例

     {}}
      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 = ;
          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: ,
        });
        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 (
           {
              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 容器之外。
    返回一个对象,通过这个对象引用来显式关闭组件
    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 ? (
          
            {this.renderBody()}
          
        ) : 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' },
      ];
    }
    
    
    

抽屉式表单组件


  • 组件定义

    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 (
          
    {cancelText ? ( ) : null} {okText ? ( ) : null}
    ); }; render() { const { children, title, width, visible, closable, formLayout, ...restProps } = this.props; return visible ? (
    {this.renderBody()}
    {this.renderFooter()}
    ) : 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' },
      ];
    }
    
    
    

搜索表单组件


  • 组件定义

    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 (
          
    {render ? render(this.props) : null}
    ); } } 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 } }) => (
        
          
            
          
          
            
              
                {getFieldDecorator('param1')()}
              
              {renderFormItem({ label: '条件2', name: 'param2' }, getFieldDecorator)}
              {renderFormItem({ label: '条件3', name: 'param3' }, getFieldDecorator)}
            
            
              
                
                
                
              
            
          
        
      );
    
      render() {
        return (
          
        );
      }
    }
    

遇到的问题


在实际使用的过程中,弹框表单的子组件中可能会包含 被 connect 的组件,光使用 antd 的弹框组件包裹就会报错:

Uncaught Error: Could not find "store" in either the context or props of "Connect(Demo)". Either wrap the root component in a , 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
  
    
  ,
  div
);

具体调用位置在上面 PopupForm.open 中,该代码已经按 dva 提供的方式进行解决了。

最后


完整代码已经传到 CodeSandbox ,点击查看 antd 版 或者 antd pro 版


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

你可能感兴趣的:(Ant Design Pro 使用之 表单组件再封装)