form

197 阅读4分钟

appfrom,

import React from 'react';
import { Form, Button } from 'antd';
import memoizeOne from 'memoize-one';
import { FormInstance } from 'antd/lib/form';

import AppFormLayout from './AppFormLayout';
import { customAppFormProps, defaultSubmitButton } from './constant';
import {
  getRestProps,
  shouldUpdateFormItems,
  normalizeFormInitialValues,
  getChangeStore,
  transformItemsInitialValue,
  shouldUpdateForm,
} from './utils';

import { Store, AppFormProps } from './interface.d';

interface AppFormState {
  // 只影响隐藏与显示的表单刷新
  showItemsUpdateTime?: number;

  // 整个表单刷新时间,某些情况下需要刷新整个表单
  formUpdateTime?: number;

  formName?: string;
}

/**
 * AppForm Class 版性能会优于 hooks 版大概 16%
 * 代码行数多于 hooks 版
 */
export default class AppForm extends React.PureComponent<AppFormProps, AppFormState> {
  private initValues: Store = {};

  private updateStore: Store = {};

  private performTime = window.performance.now();

  private formRef = React.createRef<FormInstance>();

  // 使用 memoizeOne 缓存最近一次的,计算结果,优化性能
  private shouldUpdateFormItems = memoizeOne(shouldUpdateFormItems);

  // 使用 memoizeOne 缓存最近一次的,计算结果,优化性能
  // private getFormRestProps = memoizeOne(getRestProps);

  // 使用 memoizeOne 缓存最近一次的,计算结果,优化性能
  private transformItemsInitialValue = memoizeOne(transformItemsInitialValue);

  // eslint-disable-next-line react/sort-comp
  private updateFormStore(form: FormInstance | null): boolean {
    const { updateStore, numberToString = true } = this.props;
    let ret = false;

    if (form && updateStore) {
      const changeUpdateStore = getChangeStore({ prev: this.updateStore, next: updateStore, numberToString });

      if (changeUpdateStore) {
        form.setFieldsValue(changeUpdateStore);

        this.updateStore = updateStore;
        ret = true;
      }
    }

    return ret;
  }

  constructor(props: AppFormProps) {
    super(props);
    const { name = 'appForm', formItems, initialValues, numberToString = true } = props;

    // 初始值应该只初始化一次
    this.initValues = normalizeFormInitialValues(formItems, initialValues, numberToString);

    this.state = {
      showItemsUpdateTime: -1,
      formName: name,
      formUpdateTime: -1,
    };
  }

  componentDidMount(): void {
    const { onReady } = this.props;

    // 必须 form 第一次渲染完成后才有 this.formRef.current
    if (this.formRef.current) {
      onReady && onReady(this.formRef.current);

      this.updateFormStore(this.formRef.current);

      // 依据 form 表单属性重新 render 一次
      this.setState({
        showItemsUpdateTime: Date.now(),
      });
    }
  }

  // eslint-disable-next-line camelcase
  UNSAFE_componentWillUpdate() {
    if (process.env.BUILD_MODE === 'development') {
      this.performTime = window.performance.now();
    }
  }

  componentDidUpdate(): void {
    if (process.env.BUILD_MODE === 'development') {
      window.console.log(`[AppForm] ${this.state.formName} update time =`, window.performance.now() - this.performTime);
    }

    const needChangeStore = this.updateFormStore(this.formRef.current);

    // 如果有 updateStore 则更新一下 showItems
    if (needChangeStore) {
      // eslint-disable-next-line react/no-did-update-set-state
      this.setState({
        showItemsUpdateTime: Date.now(),
      });
    }
  }

  // 处理提交
  private handSubmit = (): void => {
    const { beforeSubmit } = this.props;
    const form = this.formRef.current;

    //
    if (!form) {
      return;
    }

    if (typeof beforeSubmit === 'function') {
      beforeSubmit(form);
    }

    form.submit();
  };

  private onStoreChange = async (changeStore: Store, allStore: Store) => {
    const { onValuesChange, formItems } = this.props;
    const { formName } = this.state;
    const curForm = this.formRef.current;

    // 调用 Props 的 onValuesChange
    if (typeof onValuesChange === 'function') {
      onValuesChange(changeStore, allStore);
    }

    const shouldUpdateFormResult = await shouldUpdateForm(this.props, changeStore, allStore);
    const curUpdateId = Date.now();
    const updateState: { formUpdateTime?: number; showItemsUpdateTime?: number } = {};
    const { shouldUpdate } = this.shouldUpdateFormItems(formItems, curForm, formName, curUpdateId);

    if (shouldUpdateFormResult === true || shouldUpdate === true) {
      // 所有表单项都需要更新
      if (shouldUpdateFormResult) {
        updateState.formUpdateTime = curUpdateId;
      }

      // TODO: 如果父组件页监听表单值改变就重新渲染整个 Form,可能导致多 render 一次,思考如何优化
      if (shouldUpdate) {
        updateState.showItemsUpdateTime = curUpdateId;
      }

      this.setState(updateState);
    }
  };

  render(): React.ReactElement {
    const {
      children,
      footerClassName,
      footerStyle,
      submitButton = defaultSubmitButton,
      afterFormDOM,
      formItems,
      colSpan,
      labelCol,
      numberToString = true,
    } = this.props;
    const { type = 'default', text = '保存' } = submitButton || {};
    const restProps = getRestProps(this.props, customAppFormProps);

    // 因为 formItems 有可能在初始化后新增项,还需要处理 this.initValues 中未定义的 formItems 中的初始值
    this.transformItemsInitialValue(formItems, this.initValues, numberToString);

    const curForm = this.formRef.current;
    const { onStoreChange, handSubmit } = this;
    const { showItemsUpdateTime, formName, formUpdateTime } = this.state;
    const { showItems } = this.shouldUpdateFormItems(formItems, curForm, formName, showItemsUpdateTime);

    let newAfterFormDOM = null;

    const formItemProps = {
      form: curForm,
      showItems,
      labelCol,
      colSpan,
      formUpdateTime,
      numberToString,
    };

    // afterFormDOM 需要用到 form 完成初始化
    if (curForm) {
      if (typeof afterFormDOM === 'function') {
        newAfterFormDOM = afterFormDOM({
          form: curForm,
        });
      } else {
        newAfterFormDOM = afterFormDOM;
      }
    }

    return (
      <Form
        {...restProps}
        name={formName}
        initialValues={this.initValues}
        onValuesChange={onStoreChange}
        ref={this.formRef}
      >
        <AppFormLayout {...formItemProps} />
        {newAfterFormDOM}
        <div className={footerClassName} style={footerStyle}>
          {submitButton ? (
            <Button {...submitButton} type={type} onClick={handSubmit}>
              {text}
            </Button>
          ) : null}
          {children}
        </div>
      </Form>
    );
  }
}

AppFormFunc

import React, { useCallback, useState, useEffect, memo } from 'react';

import { Form, Button } from 'antd';
import memoize from 'memoize-one';

import { shouldUpdateFormItems, getRestProps, getChangeStore, shouldUpdateForm } from './utils';
import { customAppFormProps, defaultSubmitButton } from './constant';
import AppFormLayout from './AppFormLayout';

import { Store, AppFormProps } from './interface.d';

function usePerformanceEffect() {
  // eslint-disable-next-line compat/compat
  let time = performance.now();

  useEffect(() => {
    // eslint-disable-next-line compat/compat
    console.log('[AppForm] hooks render time = ', performance.now() - time);
  });
}

// TODO:待优化,现在多个 AppForm 渲染时,公用一份缓存
const curShouldUpdateFormItems = memoize(shouldUpdateFormItems);
let cacheUpdateStore: Store = {};

/**
 * AppForm 的 hooks 版性能要低于 class 版 16%
 * 代码量少 30% 左右
 * 尽量只做布局上的处理
 *
 * @param props
 */
function AppForm(props: AppFormProps): React.ReactElement {
  const {
    submitButton = defaultSubmitButton,
    children,
    name,
    footerClassName,
    afterFormDOM,
    formItems,
    colSpan,
    labelCol,
    numberToString = true,
    onValuesChange,
    beforeSubmit,
    onReady,
    updateStore,
  } = props;
  const { type = 'default', text } = submitButton;
  const restProps = getRestProps(props, customAppFormProps);

  // Hooks
  const [form] = Form.useForm();
  const [updateState, setFormUpdate] = useState({ formUpdateTime: -1, showItemsUpdateTime: -1 });
  const { showItems } = curShouldUpdateFormItems(formItems, form, name, updateState.showItemsUpdateTime);

  const onStoreChange = useCallback(
    (changeStore: Store, allStore: Store) => {
      if (typeof onValuesChange === 'function') {
        onValuesChange(changeStore, allStore);
      }

      shouldUpdateForm(props, changeStore, allStore).then(result => {
        const curUpdateId = Date.now();
        const newUpdateState: { formUpdateTime?: number; showItemsUpdateTime?: number } = {};
        const { shouldUpdate } = shouldUpdateFormItems(formItems, form, name, curUpdateId);

        if (result === true || shouldUpdate === true) {
          // 所有表单项都需要更新
          if (result) {
            newUpdateState.formUpdateTime = curUpdateId;
          }

          // TODO: 如果父组件页监听表单值改变也重新渲染 Form,可能导致多 render 一次,思考如何优化
          if (shouldUpdate) {
            newUpdateState.showItemsUpdateTime = curUpdateId;
          }

          setFormUpdate({
            ...updateState,
            ...newUpdateState,
          });
        }
      });
    },
    [onValuesChange, form, name]
  );

  // eslint-disable-next-line no-self-compare
  const handSubmit = useCallback(() => {
    if (typeof beforeSubmit === 'function') {
      beforeSubmit(form);
    }
    form.submit();
  }, [beforeSubmit, form]);

  // 类似 componentsDidMount
  useEffect(() => {
    onReady && onReady(form);
  }, []);

  useEffect(() => {
    let ret = false;

    if (form && updateStore) {
      const changeUpdateStore = getChangeStore({ prev: cacheUpdateStore, next: updateStore, numberToString });

      if (changeUpdateStore) {
        form.setFieldsValue(changeUpdateStore);

        cacheUpdateStore = updateStore;
        ret = true;
      }
    }

    if (ret) {
      setFormUpdate({
        ...updateState,
        formUpdateTime: Date.now(),
      });
    }
  }, [updateStore]);

  // 类似 componentWillUnmount
  useEffect(() => {
    return () => {
      console.log('componentWillUnmount');
    };
  }, []);

  usePerformanceEffect();

  const formContentProps = {
    form,
    showItems,
    labelCol,
    colSpan,
    formUpdateTime: updateState.formUpdateTime,
    numberToString,
  };

  return (
    <Form {...restProps} form={form} onValuesChange={onStoreChange}>
      <AppFormLayout {...formContentProps} />
      {typeof afterFormDOM === 'function'
        ? afterFormDOM({
            form,
          })
        : afterFormDOM}
      <div className={footerClassName}>
        {submitButton ? (
          <Button {...submitButton} type={type} onClick={handSubmit}>
            {text || '保存'}
          </Button>
        ) : null}
        {children}
      </div>
    </Form>
  );
}

export default memo(AppForm);

AppFormItem

/* eslint-disable @typescript-eslint/ban-ts-comment */
import React, { memo } from 'react';
import { getIn } from 'fezs-js';
import { Input, Form, Select, Radio, Checkbox, DatePicker, TimePicker, InputNumber, AutoComplete } from 'antd';
import { FormInstance, Rule } from 'antd/lib/form';

import {
  AppFormItemElementProps,
  AppFormItemChildProps,
  ItemFunctionKey,
  SelectListItem,
  ListFieldNames,
  Store,
  NamePath,
  FormInputAttr,
  FormItemType,
  // DOMFunction,
  // FormItemDOMFunctions,
  ItemBooleanFunction,
} from './interface.d';
import { getRestProps, createNamePathKey } from './utils';

const { TextArea } = Input;
const CheckboxGroup = Checkbox.Group;
const RadioGroup = Radio.Group;
const { RangePicker } = DatePicker;
const { RangePicker: TimeRangePicker } = TimePicker;

const itemInputComponents = {
  [FormItemType.radio]: RadioGroup,
  [FormItemType.checkbox]: CheckboxGroup,
  [FormItemType.select]: Select,
  [FormItemType.autoComplete]: AutoComplete,

  input: Input,
  textArea: TextArea,
  datePicker: DatePicker,
  rangePicker: RangePicker,
  timePicker: TimePicker,
  timeRangePicker: TimeRangePicker,
  number: InputNumber,

  plainText: 'span',
};

const FormItem = Form.Item;

function getSelectFromItem(item: SelectListItem, fieldNames?: ListFieldNames): ListFieldNames {
  if (!fieldNames) {
    return item;
  }
  return {
    value: item.value !== undefined ? item.value : item[fieldNames.value],
    label: item.label || item[fieldNames.label],
  };
}

const keyMap = {
  list: 'options',
};

// 添加,需要调用函数动态计算的值
function addDynamicValueToAttrs({
  props,
  eleAttr,
  formData,
}: {
  props: AppFormItemChildProps;
  eleAttr: FormInputAttr;
  formData: Store;
}) {
  const keyArr: ItemFunctionKey[] = ['disabled', 'placeholder', 'list'];

  keyArr.forEach(key => {
    const curValue = props[key];
    const attrKey = keyMap[key] || key;

    if (typeof curValue === 'function') {
      // eslint-disable-next-line no-param-reassign
      eleAttr[attrKey] = curValue(formData);
    } else if (typeof curValue !== 'undefined') {
      // eslint-disable-next-line no-param-reassign
      eleAttr[attrKey] = curValue;
    }
  });
}

export function InputElement(formChildProps: AppFormItemElementProps): JSX.Element {
  // 以下为 Antd Form.item 会为其直接子元素 添加 id, value, onChange 三个参数
  const newProps: AppFormItemChildProps = formChildProps as AppFormItemChildProps;
  const {
    render,
    form,
    eleAttr = {},
    fieldNames,
    type = 'input',
    numberToString = true,

    // 以下为 Antd Form.item 新增加的属性
    onChange: onItemChange,
    value,
    id,
  } = newProps;
  const formData: Store = form?.getFieldsValue(true) || {};
  const { onChange } = eleAttr;

  if (typeof render === 'function') {
    return render(newProps, form);
  }

  // 存文本特殊处理
  if (type === 'plainText') {
    return value;
  }

  // 注意 ant-design Form.Item 新增加的属性
  const commonProps = {
    onChange: (e: any) => {
      let needChangeValues: Store | void;

      // Antd 透传过来的 onChange 函数
      if (typeof onItemChange === 'function') {
        onItemChange(e);
      }

      // 自定义的 change 函数
      if (typeof onChange === 'function') {
        needChangeValues = onChange(e);
      }

      if (needChangeValues) {
        form.setFieldsValue(needChangeValues);
      }
    },
    value,
    id,
  };

  addDynamicValueToAttrs({ props: newProps, eleAttr, formData });

  const { options } = eleAttr;

  if (Array.isArray(options)) {
    options.forEach((option: ListFieldNames) => {
      const newOption = getSelectFromItem(option, fieldNames);
      const { value: val } = newOption;

      if (numberToString) {
        newOption.value = typeof val === 'number' ? String(val) : val;
      }

      Object.assign(option, newOption);
    });
  }

  const ItemInput = itemInputComponents[type] || Input;

  // TODO: 类型定义比较复杂需要依据 type 类型获取具体定义,先禁用 ts检查考虑如何优化
  // @ts-ignore
  return <ItemInput {...eleAttr} {...commonProps} />;
}

function hasRequiredRule({ rules, form }: { rules?: Rule[]; form: FormInstance; name: NamePath }) {
  if (!rules) {
    return false;
  }
  const ret = rules.some(rule => {
    if (typeof rule === 'function') {
      return rule(form).required;
    }

    return rule.required;
  });

  return ret;
}

function getShowValue(isShow?: ItemBooleanFunction, formStore: Store = {}) {
  let curShow = isShow;

  if (typeof isShow === 'function') {
    curShow = isShow(formStore);
  }

  return curShow;
}

export default function AppFormItem(props: AppFormItemElementProps): React.ReactElement | null {
  const {
    beforeDOM,
    afterDOM,
    list,
    form,
    label,
    isShow,
    formItems = [],
    required,
    labelCol,
    wrapperCol,
    rules,
    name,
    initialValue,

    // 默认把数字转换成字符串
    numberToString = true,

    dependencies = [],
  } = props;

  // 要排除自定义的属性,避免不支持的属性传递到 antd 的 FormItem
  const restProps = getRestProps(props, [
    'title',
    'beforeDOM',
    'afterDOM',
    'required',
    'render',
    'isShow',
    'eleAttr',
    'numberToString',
    'updateTime',
  ]);
  const options = { form, formItem: props };
  const itemRequired = required || hasRequiredRule({ rules, form, name });
  const hasChild = typeof list === 'function' || beforeDOM || afterDOM || (formItems && formItems.length > 0);
  let isItemShow = isShow;

  // 把数组转换成字符串
  if (numberToString === true && typeof initialValue === 'number') {
    // eslint-disable-next-line no-param-reassign
    restProps.initialValue = `${initialValue}`;
  }

  if (typeof isShow === 'function') {
    isItemShow = isShow(form.getFieldsValue(true));
  }

  if (isItemShow === false) {
    return null;
  }

  // 如果有子元素需要动态渲染的
  const childShouldUpdate = (prev: Store, next: Store) => {
    const needCheckNames = [name, ...dependencies];
    const needUpdateResult = needCheckNames.some(curName => {
      return getIn(prev, curName) !== getIn(next, curName);
    });

    return needUpdateResult;
  };

  return hasChild ? (
    <FormItem
      labelCol={labelCol}
      shouldUpdate={childShouldUpdate}
      wrapperCol={wrapperCol}
      label={label}
      required={itemRequired}
    >
      {({ getFieldsValue }) => {
        return [
          typeof beforeDOM === 'function' ? beforeDOM(options) : beforeDOM,
          <FormItem {...restProps} key={createNamePathKey(name)} noStyle>
            <InputElement {...props} />
          </FormItem>,
          formItems
            ? formItems.map(formItem => {
                // 把数组转换成字符串
                if (formItem.numberToString === true && typeof formItem.initialValue === 'number') {
                  // eslint-disable-next-line no-param-reassign
                  formItem.initialValue = String(formItem.initialValue);
                }

                return getShowValue(formItem.isShow, getFieldsValue(true)) ? (
                  <FormItem {...formItem} key={createNamePathKey(formItem.name)} noStyle>
                    <InputElement {...formItem} form={form} />
                  </FormItem>
                ) : null;
              })
            : null,
          typeof afterDOM === 'function' ? afterDOM(options) : afterDOM,
        ];
      }}
    </FormItem>
  ) : (
    <FormItem {...restProps} required={itemRequired}>
      <InputElement {...props} />
    </FormItem>
  );
}

export const PureAppFormItem = memo(AppFormItem);

AppFormLayout

import React, { memo } from 'react';
import { Row, Col } from 'antd';

import { AppFormLayoutProps } from './interface.d';
import { PureAppFormItem } from './AppFormItem';
import { createNamePathKey } from './utils';

export default function AppFormLayout(props: AppFormLayoutProps): React.ReactElement | null {
  const { colSpan = 1, form, labelCol: formLabelCol, showItems = [], numberToString, formUpdateTime } = props;

  if (!form) {
    return null;
  }

  return (
    <Row>
      {showItems.map((formItem, i) => {
        const { rowAlone, isTitle, title, labelCol, ...restItemProps } = formItem;
        const formColSpan = rowAlone ? 1 : colSpan;
        const itemColSpan = Math.floor(24 / formColSpan);
        let key = formItem.key || createNamePathKey(formItem.name);

        if (!key) {
          key = `title-${i}`;
        }

        // 标题单独渲染
        if (isTitle) {
          return (
            <Col className="form-legend" key={key} span={24}>
              {typeof title === 'function' ? title(form) : title}
            </Col>
          );
        }

        let newLabelCol = labelCol;

        // 独占行的,如果自己没有定义 formItemLayout, labelCol.span 要按列数比例缩小
        if (!labelCol && rowAlone) {
          if (formLabelCol && typeof formLabelCol.span === 'number') {
            newLabelCol = {
              span: Math.round(formLabelCol.span / colSpan),
            };
          }
        }

        // 避免传递非波尔类型,导致判断出错
        if (typeof numberToString === 'boolean' && typeof restItemProps.numberToString === 'undefined') {
          restItemProps.numberToString = numberToString;
        }

        return (
          <Col span={itemColSpan} key={key}>
            <PureAppFormItem {...restItemProps} updateTime={formUpdateTime} labelCol={newLabelCol} form={form} />
          </Col>
        );
      })}
    </Row>
  );
}

export const PureAppFormLayout = memo(AppFormLayout);

constant

import { SubmitButton } from './interface.d';

export const customAppFormProps = [
  'submitButton',
  'children',
  'footerClassName',
  'afterFormDOM',
  'onReady',
  'formItems',
  'beforeSubmit',
  'colSpan',
  'numberToString',
  'shouldFormUpdate',
  'updateStore',
  'footerStyle'
];

export const defaultSubmitButton: SubmitButton = {
  type: 'default',
  text: '保存',
};

index

// import { FormInstance } from 'antd/lib/form/index.d';

import './style.scss';

export { default as AppForm } from './AppForm';

// AppForm Hooks 版
export { default as AppFormFunc } from './AppFormFunc';

export { default as AppFormItem } from './AppFormItem';

export { default as AppFormLayout } from './AppFormLayout';

interface.d

import { FormProps, FormItemProps, FormInstance } from 'antd/lib/form/index.d';
import { ButtonProps } from 'antd/lib/button/index.d';
import { ColProps } from 'antd/lib/grid/index.d';

export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;

export interface Store {
  [name: string]: any;
}

export enum FormItemType {
  // 下拉选择框
  select = 'select',

  // 纯文本
  plainText = 'plainText',

  // input 输入框
  input = 'input',

  // 数组类输入框
  number = 'number',
  textArea = 'textArea',
  datePicker = 'datePicker',
  checkbox = 'checkbox',

  radio = 'radio',

  rangePicker = 'rangePicker',
  timePicker = 'timePicker',
  timeRangePicker = 'timeRangePicker',

  // 自动补全下拉框
  autoComplete = 'autoComplete',
}

export type ItemBooleanFunction = ((formData: Store) => boolean) | boolean;

export interface SelectListItem {
  value: string | number;
  label: string;
  [random: string]: any;
}

export interface ListFieldNames {
  value: string | number;
  label: string;
  disabled?: boolean;
}

export interface FormIsShowData {
  [name: string]: boolean;
}

export interface DomOptions {
  form: FormInstance;
  formItem: AppFormItemElementProps;
}

export type FormItemDOMFunction = ((options: DomOptions) => JSX.Element | string | null) | JSX.Element | null | string; // data 是表单的数据

export type FormDOMFunction =
  | ((options: { form: FormInstance }) => JSX.Element | string | null)
  | JSX.Element
  | null
  | string; // data 是表单的数据

export interface FormItemsUpdateResult {
  showItems: AppFormItemOptions[];
  shouldUpdate: boolean;
}

type onItemChange = (e: any) => Store | void;

export interface FormInputAttr {
  maxLength?: number;
  readOnly?: boolean;
  disabled?: boolean;
  style?: React.CSSProperties;
  placeholder?: string;

  /**
   * 运行后,无返回值则不做其他处理
   *
   * 返回对象,则会调用 form.setFieldsValue(ret), 把返回对象合并到 form Store 中
   */
  onChange?: onItemChange;
  onBlur?: (e: any) => void;
  onClick?: (e: any) => void;
  filterOption?: boolean | ((inputValue: any, option: any) => boolean);
  [k: string]: any;
}

export type ItemFunctionKey = 'disabled' | 'isShow' | 'placeholder' | 'list';

type RenderDom = JSX.Element | string | null;

// 表单元素的子表单元素,用于表单项的子表单项 item.formItems
// 子项不支持 beforeDOM afterDOM rowAlone formItems 属性
export interface AppFormItemBase extends FormItemProps {
  name: string | number | InternalNamePath;

  numberToString?: boolean;

  key?: string;
  label?: string | ReactNode;

  type?: keyof typeof FormItemType;

  // 有一些需要依据表单值动态渲染,可以传自定义函数
  isShow?: ItemBooleanFunction;
  disabled?: ItemBooleanFunction;
  placeholder?: ItemBooleanFunction;

  shouldUpdate?(prev: Store, next: Store): boolean;

  /**
   * 运行后,无返回值则不做其他处理
   *
   * 返回对象,则会调用 form.setFieldsValue(ret), 把返回对象合并到 form Store 中
   */
  onChange?: onItemChange;

  // 支持函数返回值或直接列表
  fieldNames?: ListFieldNames;
  list?: IList[] | ((allValues: Store) => IList[]); // 注意函数不要写异步函数, 不要在里面请求接口, 只做数据处理; // allValues: 表单数据;

  // 用于直接配置表单 input 组件属性
  eleAttr?: FormInputAttr;

  // 内部定义使用,不需要传
  children?: React.ReactElement;
}

export interface FormItemDOMFunctions {
  beforeDOM?: FormItemDOMFunction;
  afterDOM?: FormItemDOMFunction;
}

export interface AppFormItemOptions extends AppFormItemBase, FormItemDOMFunctions {
  rowAlone?: boolean;

  isTitle?: boolean;
  title?: ((form: FormInstance) => RenderDom) | RenderDom;


  // 自定义表单项渲染函数
  render?: (formItem: AppFormItemChildProps, form: FormInstance) => React.ReactElement;

  formItems?: AppFormItemBase[];
}

export interface AppFormLayoutProps {
  // antd 表单实例
  form: FormInstance | null;

  // 具体项的配置列表
  showItems: AppFormItemOptions[];

  // 统一控制所有表单项的布局,具体表单的自定义布局优先级更高
  labelCol?: ColProps;
  wrapperCol?: ColProps;

  // 表单元素布局一行几列
  colSpan?: number;

  // 表单值是否 number 转 字符串,list 值不做转换
  numberToString?: boolean;

  // 不建议使用:强制更新时间,用于特殊情况强制更新所有 表单
  formUpdateTime?: number;
}

// 用户与内部渲染的属性
export interface AppFormItemElementProps extends AppFormItemOptions {
  form: FormInstance;
  updateTime?: number;
}

export interface AppFormItemChildProps extends AppFormItemElementProps {
  id: string;
  value: any;
  onChange(ev: any): Store | void;
}

export interface SubmitButton extends ButtonProps {
  text: string;
}

export interface FormInitialValuesOption {
  formItems: AppFormItemOptions[];
  numberToString?: boolean;
  initialValues?: Store;
}

type shouldFormUpdate = (
  changeStore: Store,
  allValues?: Store,
  form?: FormInstance | null
) => boolean | Promise<boolean>;
export interface AppFormProps extends FormProps {
  // 表单项列表
  formItems: AppFormItemOptions[];

  // 通过对象来更新 form 的 Store
  // 例如:数据是异步获取,需要过一段时间再更新表单值
  updateStore?: Store;

  // 定义一行有几列
  colSpan?: 1 | 2 | 3 | 4;

  // 自定义 dom
  afterFormDOM?: FormDOMFunction;

  // 用于控制整个表单 强制更新
  shouldFormUpdate?: shouldFormUpdate;

  // 自定义 footer 样式控制类
  footerClassName?: string;
  footerStyle?: React.CSSProperties;

  // 提交按钮点击,表单提交前调用函数
  beforeSubmit?(form: FormInstance): void;

  // 配置提交按钮内容
  submitButton?: SubmitButton | null;

  // 表单实例准备好后调用
  onReady?(form: FormInstance): void;

  // 是否开启数字转换字符串
  numberToString?: boolean;
}

createNamePathKey

import { NamePath } from '../interface.d';

export default function createNamePathKey(namePath: NamePath): number | string {
  let ret: number | string;

  if (Array.isArray(namePath)) {
    ret = namePath.join('.');
  } else {
    ret = namePath;
  }

  return ret;
}

getChangeStore

import { Store } from '../interface.d';
import getValue from './getItemValue';

interface ChangeProps {
  prev: Store;
  next?: Store;
  showKeyMap?: Store;
  numberToString: boolean;
}

// 获取 update Store 变化
export default function getChangeStore({ prev, next, showKeyMap, numberToString }: ChangeProps): null | Store {
  let ret: Store = {};

  if (!next) {
    return null;
  }

  if (prev) {
    const nextKeys = next ? Object.keys(next) : [];

    nextKeys.forEach(curKey => {
      let nextVal = next[curKey];
      const prevVal = prev[curKey];

      nextVal = getValue(nextVal, numberToString);

      if (typeof nextVal !== 'undefined' && nextVal !== prevVal) {
        ret[curKey] = nextVal;
        // 更新值后需要同步一下,避免多次渲染
        next[curKey] = nextVal;
      }
    });
  } else {
    ret = next;
  }

  return Object.keys(ret).length > 0 ? ret : null;
}

getItemValue

export default function getInitialValue(val: any, numberToString?: boolean) {
  let ret = val;

  if (numberToString && typeof val === 'number') {
    ret = `${val}`;
  }

  return ret;
}

getRestProps


/**
 * 获取剩余的属性,避免多余属性穿透子组件
 *
 * @export
 * @template T
 * @param {T} props
 * @param {string[]} keys
 * @returns {T}
 */
export default function getRestProps<T>(props: T, keys: string[]): T {
  let rest: T = props;

  if (keys.length > 0) {
    // 使用新的对象,避免修改原对象,带来可不预知的问题
    rest = {
      ...props,
    }
    keys.forEach(key => {
      delete rest[key];
    });
  }

  return rest;
}

index

export { default as normalizeFormInitialValues } from './normalizeFormInitialValues';
export { default as getRestProps } from './getRestProps';
export { default as shouldUpdateFormItems } from './shouldUpdateFormItems';
export { default as transformItemsInitialValue } from './transformItemsInitialValue';
export { default as getChangeStore } from './getChangeStore';
export { default as createNamePathKey } from './createNamePathKey';
export { default as shouldUpdateForm } from './shouldUpdateForm';
export { default as getItemValue } from './getItemValue';

normalizeFormInitialValues

/* eslint-disable no-param-reassign */
import { setIn } from 'fezs-js';

import { AppFormItemOptions, Store } from '../interface.d';
import traversalFormItems from './traversalFormItems';
import getInitialValue from './getItemValue';


/**
 * 合并与 AppForm 的初始值
 *
 * @export
 * @param {AppFormItemOptions[]} formItems
 * @param {Store} [initialValues]
 * @param {boolean} [numberToString]
 * @returns {Store}
 */
export default function normalizeFormInitialValues(
  formItems: AppFormItemOptions[],
  initialValues?: Store,
  numberToString?: boolean
): Store {
  const initValues = {};

  if (initialValues) {
    Object.keys(initialValues).forEach(name => {
      const curVal = initialValues[name];

      if (typeof curVal !== 'undefined') {
        initValues[name] = getInitialValue(curVal, numberToString);
      }
    });
  }

  // 需要合并初始值,并删除item中多余的初始值
  traversalFormItems(formItems, item => {
    const { initialValue, name } = item;

    if (typeof initialValue !== 'undefined') {
      const newValue = getInitialValue(initialValue, numberToString);

      // item.initialValue = newValue;

      setIn(initValues, name, newValue);

      // 避免冲突
      delete item.initialValue;
    }
  });

  return initValues;
}

shouldUpdateForm

import { AppFormProps, Store } from '../interface';

/**
 * 需要计算是否需要更新整个 form
 * shouldFormUpdate 调用出错默认返回 Promise<false>
 *
 * @export
 * @param {AppFormProps} props
 * @param {Store} changeStore
 * @param {allStore} allStore
 * @returns {Promise<boolean>}
 */
export default function shouldUpdateForm(
  props: AppFormProps,
  changeStore: Store,
  allStore: Store
): Promise<boolean> {
  const { shouldFormUpdate } = props;

  if (typeof shouldFormUpdate === 'function') {
    return new Promise((resolve: (val: boolean) => void) => {
      const needUpdate = shouldFormUpdate(changeStore, allStore);

      if (typeof needUpdate === 'boolean') {
        resolve(needUpdate);
      } else if (needUpdate && typeof needUpdate.then === 'function') {
        needUpdate
          .then(val => {
            resolve(val);
          })

          // 错误也默认 false
          .catch(() => resolve(false));
      } else {
        resolve(false);
      }
    }).catch(() => false);
  }

  return Promise.resolve(false);
}


shouldUpdateFormItems



import { FormInstance } from 'antd/lib/form';
import { getIn } from 'fezs-js';

import { FormIsShowData, FormItemsUpdateResult, AppFormItemOptions, Store,  } from '../interface.d';
import createNamePathKey from './createNamePathKey'

const showData: { [key: string]: FormIsShowData } = {};

export function getShowFormItems(curFormName: string, formStore: Store, formItems: AppFormItemOptions[]): FormItemsUpdateResult {
  let shouldUpdate = false;

  if (!showData[curFormName]) {
    showData[curFormName] = {};
  }

  const newShowData: FormIsShowData = {};
  const cacheShowData = showData[curFormName];

  const recursionItems = (formItem: AppFormItemOptions, index: number) => {
    const { name, formItems: childrenItems, key } = formItem;

    // name 有可能会重复,需要再优化一下 使用 key + name 混合
    let itemKey = key || createNamePathKey(name);

    // 出现重复的 name, React Element key 是不能重复的
    if (typeof getIn(newShowData, name) === 'boolean') {
      itemKey = `${createNamePathKey(name)}${index}`;

      // eslint-disable-next-line no-param-reassign
      formItem.key = itemKey;
    }

    const cacheShowValue: boolean | undefined = cacheShowData[itemKey];
    let { isShow = true } = formItem;

    if (typeof isShow === 'function') {
      isShow = isShow(formStore);

      newShowData[itemKey] = isShow;

      if (cacheShowValue !== isShow) {
        shouldUpdate = true;
      }
    }

    // 子项也要判断
    if (childrenItems) {
      childrenItems.forEach(recursionItems);

      // 由于 AppFormItem 是 PureComponent,要修改值才会重新渲染
      // eslint-disable-next-line no-param-reassign
      formItem.formItems = [...childrenItems];
    }

    return isShow;
  };

  // 过滤掉需要隐藏的 formItems
  const filterShowItems: AppFormItemOptions[] = formItems.filter(recursionItems);

  showData[curFormName] = newShowData;

  return {
    shouldUpdate,
    showItems: filterShowItems,
  };
}

/**
 * 隐藏与显示会影响布局, 其实只有多列布局时才需更新布局
 *
 * @param {AppFormItemOptions[]} formItems
 * @param {FormInstance} form
 * @param {number} colSpan
 * @param {string} [formName]
 * @returns
 */
export default function shouldUpdateFormItems(
  formItems: AppFormItemOptions[],
  form: FormInstance | null,
  formName?: string,
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  _id?: number,
): FormItemsUpdateResult {
  // form 实例还没有,隐藏显示必须要使用 form.getFieldsValue() 值
  // showItems 返回空
  if (!form) {
    return {
      shouldUpdate: false,
      showItems: [],
    };
  }

  const formStore = form.getFieldsValue(true);
  const curFormName = formName || 'raForm';

  return getShowFormItems(curFormName, formStore, formItems);
}

transformItemsInitialValue

/* eslint-disable no-param-reassign */
import { getIn } from 'fezs-js';
import { AppFormItemOptions, Store } from '../interface.d';
import traversalFormItems from './traversalFormItems';

/**
 * 清除 item 中 initialValue 已定义的项
 * 转换 initialValues 未定义的 initialValue 值
 *
 * @param {AppFormItemOptions[]} items
 * @param {Store} initialValues
 * @param {boolean} [numberToString]
 */
export default function transformItemsInitialValue(
  items: AppFormItemOptions[],
  initialValues: Store,
  numberToString?: boolean
) {
  traversalFormItems(items, item => {
    const { initialValue, name } = item;

    if (typeof getIn(initialValues, name) !== 'undefined') {
      delete item.initialValue;
    } else if (numberToString && typeof initialValue === 'number') {
      item.initialValue = `${initialValue}`;
    }
  });
}

traversalFormItems

import { AppFormItemOptions } from '../interface.d';

/**
 * 递归遍历 items 对象中的所有 item
 *
 * @param {AppFormItemOptions[]} items
 * @param {Store} initialValues
 * @param {boolean} [numberToString]
 */
export default function traversalFormItems(items: AppFormItemOptions[], func: (item: AppFormItemOptions, index: number) => void) {
  items.forEach((item, index) => {
    const { formItems } = item;

    func(item, index);

    if (formItems) {
      traversalFormItems(formItems, func);
    }
  });
}