美文网首页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