从antdPro的SchemaForm学习组件封装思路及一些使用经验分享

最近开发一个全新的项目,由于项目时间比较紧张,选用了开箱即用的antd ProComponents来开发表单,引发了一些小小思考,在此记录一下📝~

一、浅谈表单封装

作为前端人儿,一般都对表单再熟悉不过了。如果直接用 Form.Item去写每一个表单项,不仅代码臃肿,而且不利于复用。对于开发有一定设计规范(具有比较稳定的风格和预设行为)的软件系统而言,都是可以二次封装表单,再通过写json的形式配置出批量表单的。

表单思维导图.png

二、谈论使用SchemaForm的感受和学习到的地方

1、之前用material ui封装表单的问题

① 子组件核心表单项的props平铺展开暴露给父组件,与其他配置项属性混杂,不利于后期维护;

② 表单项没有配置slot,不利于拓展;

③ 没有向顶层组件暴露form实例的属性及方法,单纯依靠定义多个setState获取所有表单数值、当前变化表单数据、提交时校验时的首个错误表单项等,数据一层层传递很麻烦;

④ 多层组件的字段和单层组件定义不同,无法直接简单配置出混合单层及多层的表单

······

2、从使用SchemaForm引发的一些思考

SchemaForm 是根据 JSON Schema 来生成表单的工具。SchemaForm 会根据 valueType 来映射成不同的表单项。

封装组件时要注重可拓展性,支持上层组件灵活调整显示效果,以支持更多更复杂的需求;

  • 枚举类的配置项不要写死,尽量支持自定义。如SchemaForm依赖valueType映射表单项,不仅自定义了一些预设type,还支持自定义。
  • 预留slot/planB,支持自定义不同条件下的展示组件。如SchemaForm定义了renderFormItem,自定义编辑模式,返回一个 ReactNode,会自动包裹 value 和 onChange。
  • 定义更灵活的数据结构,支持map数据类型/多层数组等。如SchemaForm的columns支持多重“套娃🪆🪆”,valueType也有group类型,支持简单配置出多层复杂的表单组件;

举个官网上的🌰

const columns = [
    {
      title: '列表',
      valueType: 'formList',
      dataIndex: 'list',
      initialValue: [{ state: 'all', title: '标题' }],
      columns: [
        {
          valueType: 'group',
          columns: [
            {
              title: '状态',
              dataIndex: 'state',
              valueType: 'select',
              width: 'xs',
              valueEnum,
            },
            {
              title: '标题',
              dataIndex: 'title',
              formItemProps: {
                rules: [
                  {
                    required: true,
                    message: '此项为必填项',
                  },
                ],
              },
              width: 'm',
            },
          ],
        },
      ],
    },
 ];

三、基于SchemaForm封装更符合项目特点的表单–思路分享

由于antd-pro的SchemaForm在数据联动、数据转化、数据校验、样式布局、复杂表单项显示等各个方面都预设了很多行为,一般这些方面直接拿来用即可,所以基于SchemaForm封装表单,我们一般要做的就是结合项目特点做一些统一配置,如

  • 自行封装其他valueType方便快速使用;
  • 在columns传递给SchemaForm时做一些统一处理以免去重复配置,注意仍要支持上层组件自定义。

代码示例🌰

  • 定义ProFormProvider组件,运用useContext预设更多valueType,以供不同外层容器中的表单使用。
import { useContext } from 'react';
import { ProProvider } from '@ant-design/pro-provider';
import { Counter, InputWithSelectBefore, RadioWithInput } from './components';
import { ValueTypes } from '.';
import { ProFormProviderContext } from './context';

export const ProFormProvider = (props) => {
    const { children, valueTypeMap = {}, formRef, ...rest } = props;
    const providerValues = useContext(ProProvider);
    return (
        <ProFormProviderContext.Provider value={{ formRef, ...rest }}>
            <ProProvider.Provider
                value={{
                    ...providerValues,
                    valueTypeMap: {
                        ...valueTypeMap,
                        [ValueTypes.counter]: {
                            render: (text) => (text),
                            renderFormItem: (text, props, dom) => (
                                <Counter {...props} {...props?.fieldProps} />
                            ),
                        },
                    }
                }}>
                {children}
            </ProProvider.Provider>
        </ProFormProviderContext.Provider>
    );
};
  • 定义自定义表单项组件,关注value和onChange

可参考官网示例🌰——自定义valueType

  • 外层直接使用的SchemaForm组件,注意预设配置要支持上层组件修改噢
import { useMemo, memo } from 'react';
import { BetaSchemaForm } from '@ant-design/pro-components';
import { ProFormProvider } from '@/components';
import { useTranslation } from 'react-i18next';
import { transformColumns } from './util';

const SchemaForm = (props) => {
  const {
    layoutType = 'Form',
    size = 'large',
    initialValues,
    columns,
    formRef,
    valueTypeMap,
    submitter = false,
    onFinish,
    onValuesChange,
    ...rest
  } = props;
  const { t } = useTranslation();

 // 利用transformColumns方法做一些统一处理。
  const _columns = useMemo(() => {
    return transformColumns(columns, t);
  }, [columns]);

  return (
    <ProFormProvider valueTypeMap={valueTypeMap} formRef={formRef} >
      <BetaSchemaForm
        autoFocusFirstInput={false}
        formRef={formRef}
        layoutType={layoutType}
        size={size}
        grid={true}
        rowProps={{ gutter: [30, 0] }}
        labelCol={{ flex: '0 0 40px' }}
        initialValues={initialValues}
        columns={_columns}
        submitter={submitter}
        onFinish={async (values) => {
          onFinish && onFinish(values);
        }}
        onValuesChange={(changeValues, values) => {
          onValuesChange && onValuesChange(changeValues, values);
        }}
        {...rest}
      />
    </ProFormProvider>
  );
};

transformColumns 参考~ 请结合公司情况做更改~

/** 统一处理表单项 */
export const transformColumns = (
  columns: IFormColumnsType<any, CustomValueType>[],
  t: TFunction,
): ProFormColumnsType<any, CustomValueType>[] => {
  return (
    columns?.map((item) => {
      const {
        title,
        translateTitle = true,
        valueType = 'text',
        isPositiveInt = true,
        required = true,
        maxLength = 200,
        disabled = false,
        fieldProps,
        colProps,
        rowProps,
        formItemProps,
        pattern,
        columns,
      } = item
      const isGroup = valueType === 'group'

      // 处理fieldProps
      const groupFieldProps = isGroup
        ? {
            collapsible: true,
          }
        : {}

      const digitFieldProps =
        valueType === 'digit'
          ? {
              style: { width: '100%' },
            }
          : {}

      const selectFieldProps =
        valueType === 'select'
          ? {
              showSearch: true,
              optionFilterProp: 'label',
              filterOption: filterOptionOfSelect,
            }
          : {}

      const commonFieldProps = {
        disabled,
        ...groupFieldProps,
        ...digitFieldProps,
        ...selectFieldProps,
        ...(fieldProps || {}),
      }

      // 默认placeholder
      const initPlaceholder =
        valueType === 'select' ||
        valueType === 'expandSelect' ||
        valueType === 'selectGroup' ||
        valueType === 'customCascader'
          ? t('global.pleaseSelect')
          : t('global.pleaseEnter')
      const _fieldProps = disabled
        ? // 禁用时,将placeholder设置为空字符串
          {
            ...commonFieldProps,
            placeholder: ' ',
          }
        : {
            ...commonFieldProps,
            // eslint-disable-next-line @typescript-eslint/ban-ts-comment
            // @ts-expect-error
            placeholder: fieldProps?.placeholder || initPlaceholder,
          }

      // 处理最终返回值
      let returnValue = {
        ...item,
        title: isString(title) && translateTitle ? t(title) : title,
        // 处理group类型的colProps和rowProps
        colProps: isGroup ? { md: 24, ...colProps } : colProps,
        rowProps: isGroup ? { gutter: 20, ...rowProps } : {},
        formItemProps: isFunction(formItemProps)
          ? formItemProps
          : {
              ...(formItemProps || {}),
              rules: [
                {
                  required,
                  message: t('global.text.required'),
                },
                pattern && {
                  pattern,
                  message: t('global.incorrectFormat'),
                },
                // counter组件必填时默认只允许输入正整数
                required &&
                  valueType === ValueTypes.counter &&
                  isPositiveInt && {
                    pattern: positiveIntPattern,
                    message: t('global.error.quantityPositiveInt'),
                  },
                // 输入框最大长度默认为200
                (valueType === 'text' || valueType === 'textarea') && {
                  max: maxLength,
                  message: t('global.validate.massage.maxLength', { length: maxLength, count: maxLength }),
                },
                ...(formItemProps?.rules || []),
              ].filter(Boolean),
            },
        fieldProps: isFunction(fieldProps) ? fieldProps : _fieldProps,
      }

      // 处理columns是数组的情况
      returnValue =
        isArray(columns) && columns.length > 0
          ? {
              ...returnValue,
              columns: transformColumns(columns, t),
            }
          : returnValue

      // 处理columns是函数的情况
      returnValue = isFunction(columns)
        ? {
            ...returnValue,
            columns: (record) => transformColumns(columns(record), t),
          }
        : returnValue

      return returnValue
    }) || []
  )
}

四、参考文章

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值