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);
}
});
}