From 63698fb3f1802469ea3f13dc6a0c5012d4fe6cbf Mon Sep 17 00:00:00 2001 From: Elvis Liao Date: Wed, 16 Oct 2024 02:44:48 +0800 Subject: [PATCH] fix(Form): fixed issue #3076 --- src/form/Form.tsx | 8 +++ src/form/FormItem.tsx | 24 +++---- src/form/hooks/useFormItemInitialData.ts | 84 ++++++++++++++++++++++++ src/form/hooks/useInstance.tsx | 25 ++++++- src/form/type.ts | 8 +++ src/form/useInitialData.ts | 75 --------------------- src/form/utils/index.ts | 52 ++++++++------- 7 files changed, 160 insertions(+), 116 deletions(-) create mode 100644 src/form/hooks/useFormItemInitialData.ts delete mode 100644 src/form/useInitialData.ts diff --git a/src/form/Form.tsx b/src/form/Form.tsx index 054513b7b6..cfebc80d51 100644 --- a/src/form/Form.tsx +++ b/src/form/Form.tsx @@ -63,6 +63,14 @@ const Form = forwardRefWithStatics( form?.getInternalHooks?.(HOOK_MARK)?.flashQueue?.(); }, [form]); + // form 卸载时清空 floatingFormData + React.useEffect( + () => () => { + form.clearFloatingFormData(); + }, + [form], + ); + function onResetHandler(e: React.FormEvent) { [...formMapRef.current.values()].forEach((formItemRef) => { formItemRef?.current.resetField(); diff --git a/src/form/FormItem.tsx b/src/form/FormItem.tsx index 88194429d8..beabb1b5c8 100644 --- a/src/form/FormItem.tsx +++ b/src/form/FormItem.tsx @@ -2,6 +2,7 @@ import React, { forwardRef, ReactNode, useState, useImperativeHandle, useEffect, import isObject from 'lodash/isObject'; import isString from 'lodash/isString'; import get from 'lodash/get'; +import unset from 'lodash/unset'; import merge from 'lodash/merge'; import isFunction from 'lodash/isFunction'; import { @@ -25,8 +26,8 @@ import { HOOK_MARK } from './hooks/useForm'; import { validate as validateModal, parseMessage } from './formModel'; import { useFormContext, useFormListContext } from './FormContext'; import useFormItemStyle from './hooks/useFormItemStyle'; +import useFormItemInitialData, { ctrlKeyMap } from './hooks/useFormItemInitialData'; import { formItemDefaultProps } from './defaultProps'; -import { ctrlKeyMap, getDefaultInitialData } from './useInitialData'; import { ValidateStatus } from './const'; import useDefaultProps from '../hooks/useDefaultProps'; import { useLocaleReceiver } from '../locale/LocalReceiver'; @@ -62,7 +63,6 @@ const FormItem = forwardRef((originalProps, ref form, colon, layout, - initialData: FormContextInitialData, requiredMark: requiredMarkFromContext, labelAlign: labelAlignFromContext, labelWidth: labelWidthFromContext, @@ -76,12 +76,9 @@ const FormItem = forwardRef((originalProps, ref onFormItemValueChange, } = useFormContext(); - const { - name: formListName, - rules: formListRules, - formListMapRef, - initialData: FormListInitialData, - } = useFormListContext(); + const { name: formListName, rules: formListRules, formListMapRef } = useFormListContext(); + + const { getDefaultInitialData } = useFormItemInitialData(); const props = useDefaultProps(originalProps, formItemDefaultProps); @@ -115,13 +112,15 @@ const FormItem = forwardRef((originalProps, ref const [formValue, setFormValue] = useState( getDefaultInitialData({ name, - formListName, children, initialData, - FormContextInitialData, - FormListInitialData, }), ); + // 组件渲染后删除对应游离值 + useEffect(() => { + const nameList = formListName ? [formListName, name].flat() : name; + unset(form.floatingFormData, nameList); + }, [form.floatingFormData, formListName, name]); const formItemRef = useRef(); // 当前 formItem 实例 const innerFormItemsRef = useRef([]); @@ -326,11 +325,8 @@ const FormItem = forwardRef((originalProps, ref if (resetType === 'initial') { return getDefaultInitialData({ name, - formListName, children, initialData, - FormContextInitialData, - FormListInitialData, }); } diff --git a/src/form/hooks/useFormItemInitialData.ts b/src/form/hooks/useFormItemInitialData.ts new file mode 100644 index 0000000000..20d5f81288 --- /dev/null +++ b/src/form/hooks/useFormItemInitialData.ts @@ -0,0 +1,84 @@ +import React from 'react'; +import get from 'lodash/get'; + +// 兼容特殊数据结构和受控 key +import Tree from '../../tree/Tree'; +import Upload from '../../upload/upload'; +import CheckTag from '../../tag/CheckTag'; +import Checkbox from '../../checkbox/Checkbox'; +import TagInput from '../../tag-input/TagInput'; +import RangeInput from '../../range-input/RangeInput'; +import Transfer from '../../transfer/Transfer'; +import CheckboxGroup from '../../checkbox/CheckboxGroup'; +import DateRangePicker from '../../date-picker/DateRangePicker'; +import TimeRangePicker from '../../time-picker/TimeRangePicker'; + +import { useFormContext, useFormListContext } from '../FormContext'; +import { FormItemProps } from '../FormItem'; + +// FormItem 子组件受控 key +export const ctrlKeyMap = new Map(); +ctrlKeyMap.set(Checkbox, 'checked'); +ctrlKeyMap.set(CheckTag, 'checked'); +ctrlKeyMap.set(Upload, 'files'); + +// FormItem 默认数据类型 +export const initialDataMap = new Map(); +[Tree, Upload, Transfer, TagInput, RangeInput, CheckboxGroup, DateRangePicker, TimeRangePicker].forEach((component) => { + initialDataMap.set(component, []); +}); +[Checkbox].forEach((component) => { + initialDataMap.set(component, false); +}); + +export default function useFormItemInitialData() { + const { form, initialData: formContextInitialData } = useFormContext(); + const { floatingFormData } = form; + + const { name: formListName, initialData: formListInitialData } = useFormListContext(); + + // 整理初始值 优先级:Form.initialData < FormList.initialData < FormItem.initialData < floatFormData + function getDefaultInitialData({ + name, + children, + initialData, + }: { + name: FormItemProps['name']; + children: FormItemProps['children']; + initialData: FormItemProps['initialData']; + }) { + if (name && floatingFormData) { + const nameList = formListName ? [formListName, name].flat() : name; + const defaultInitialData = get(floatingFormData, nameList); + if (typeof defaultInitialData !== 'undefined') return defaultInitialData; + } + + if (initialData) { + return initialData; + } + + if (name && formListInitialData.length) { + const defaultInitialData = get(formListInitialData, name); + if (typeof defaultInitialData !== 'undefined') return defaultInitialData; + } + + if (name && formContextInitialData) { + const defaultInitialData = get(formContextInitialData, name); + if (typeof defaultInitialData !== 'undefined') return defaultInitialData; + } + + if (typeof children !== 'function') { + const childList = React.Children.toArray(children); + const lastChild = childList[childList.length - 1]; + if (lastChild && React.isValidElement(lastChild)) { + // @ts-ignore + const isMultiple = lastChild?.props?.multiple; + return isMultiple ? [] : initialDataMap.get(lastChild.type); + } + } + } + + return { + getDefaultInitialData, + }; +} diff --git a/src/form/hooks/useInstance.tsx b/src/form/hooks/useInstance.tsx index 69079d83c1..8ab931dd57 100644 --- a/src/form/hooks/useInstance.tsx +++ b/src/form/hooks/useInstance.tsx @@ -1,6 +1,9 @@ import isEmpty from 'lodash/isEmpty'; import isFunction from 'lodash/isFunction'; import merge from 'lodash/merge'; +import get from 'lodash/get'; +import set from 'lodash/set'; +import { useRef } from 'react'; import type { TdFormProps, FormValidateResult, @@ -10,7 +13,7 @@ import type { NamePath, } from '../type'; import useConfig from '../../hooks/useConfig'; -import { getMapValue, travelMapFromObject, calcFieldValue } from '../utils'; +import { getMapValue, objectToArray, travelMapFromObject, calcFieldValue } from '../utils'; import log from '../../_common/js/log'; // 检测是否需要校验 默认全量校验 @@ -42,6 +45,7 @@ function formatValidateResult(validateResultList) { export default function useInstance(props: TdFormProps, formRef, formMapRef: React.MutableRefObject>) { const { classPrefix } = useConfig(); + const floatingFormDataRef = useRef>({}); const { scrollToFirstError, preventSubmitDefault = true, onSubmit, onReset } = props; @@ -139,8 +143,16 @@ export default function useInstance(props: TdFormProps, formRef, formMapRef: Rea // 对外方法,设置对应 formItem 的值 function setFieldsValue(fields = {}) { - travelMapFromObject(fields, formMapRef, (formItemRef, fieldValue) => { - formItemRef?.current?.setValue?.(fieldValue, fields); + const nameLists = objectToArray(fields); + + nameLists.forEach((nameList) => { + const fieldValue = get(fields, nameList); + const formItemRef = formMapRef.current.get(nameList.length > 1 ? nameList : nameList[0]); + if (formItemRef?.current) { + formItemRef?.current?.setValue?.(fieldValue, fields); + } else { + set(floatingFormDataRef.current, nameList, fieldValue); + } }); } @@ -198,6 +210,11 @@ export default function useInstance(props: TdFormProps, formRef, formMapRef: Rea }); } + // 对外方法,清空 floatingFormData + function clearFloatingFormData() { + floatingFormDataRef.current = {}; + } + return { submit, reset, @@ -211,5 +228,7 @@ export default function useInstance(props: TdFormProps, formRef, formMapRef: Rea getFieldsValue, currentElement: formRef.current, getCurrentElement: () => formRef.current, + floatingFormData: floatingFormDataRef.current, + clearFloatingFormData, }; } diff --git a/src/form/type.ts b/src/form/type.ts index 909bfb6cc3..fee1abf2f2 100644 --- a/src/form/type.ts +++ b/src/form/type.ts @@ -153,6 +153,14 @@ export interface FormInstanceFunctions { * 纯净的校验函数,仅返回校验结果,不对组件进行任何操作。泛型 `FormData` 表示表单数据 TS 类型。参数和返回值含义同 `validate` 方法 */ validateOnly: (params?: Pick) => Promise>; + /** + * 游离 formData,若调用 setFieldsValue 设置值对应的组件还没渲染,会暂存为游离值 + */ + floatingFormData?: Record; + /** + * 重置游离 formData + */ + clearFloatingFormData?: () => void; } export interface TdFormItemProps { diff --git a/src/form/useInitialData.ts b/src/form/useInitialData.ts deleted file mode 100644 index e1e025eea6..0000000000 --- a/src/form/useInitialData.ts +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import get from 'lodash/get'; -import isFunction from 'lodash/isFunction'; - -// 兼容特殊数据结构和受控 key -import Tree from '../tree/Tree'; -import Upload from '../upload/upload'; -import CheckTag from '../tag/CheckTag'; -import Checkbox from '../checkbox/Checkbox'; -import TagInput from '../tag-input/TagInput'; -import RangeInput from '../range-input/RangeInput'; -import Transfer from '../transfer/Transfer'; -import CheckboxGroup from '../checkbox/CheckboxGroup'; -import DateRangePicker from '../date-picker/DateRangePicker'; -import TimeRangePicker from '../time-picker/TimeRangePicker'; - -import { FormItemProps } from './FormItem'; -import { TdFormListProps, TdFormProps } from './type'; - -// FormItem 子组件受控 key -export const ctrlKeyMap = new Map(); -ctrlKeyMap.set(Checkbox, 'checked'); -ctrlKeyMap.set(CheckTag, 'checked'); -ctrlKeyMap.set(Upload, 'files'); - -// FormItem 默认数据类型 -export const initialDataMap = new Map(); -[Tree, Upload, Transfer, TagInput, RangeInput, CheckboxGroup, DateRangePicker, TimeRangePicker].forEach((component) => { - initialDataMap.set(component, []); -}); -[Checkbox].forEach((component) => { - initialDataMap.set(component, false); -}); - -// 整理初始值 优先级:Form.initialData < FormList.initialData < FormItem.initialData -export function getDefaultInitialData({ - name, - formListName, - children, - initialData, - FormContextInitialData, - FormListInitialData, -}: { - name: FormItemProps['name']; - formListName: TdFormListProps['name']; - children: FormItemProps['children']; - initialData: FormItemProps['initialData']; - FormContextInitialData: TdFormProps['initialData']; - FormListInitialData: TdFormListProps['initialData']; -}) { - let defaultInitialData; - if (FormContextInitialData) { - if (typeof name === 'string') defaultInitialData = get(FormContextInitialData, name); - if (Array.isArray(name)) { - const nameList = formListName ? [formListName, name].flat() : name; - defaultInitialData = get(FormContextInitialData, nameList); - } - } - if (FormListInitialData.length) { - defaultInitialData = get(FormListInitialData, name); - } - if (typeof initialData !== 'undefined') defaultInitialData = initialData; - - if (!isFunction(children)) { - React.Children.forEach(children, (child) => { - if (child && React.isValidElement(child) && typeof defaultInitialData === 'undefined') { - // @ts-ignore - const isMultiple = child?.props?.multiple; - defaultInitialData = isMultiple ? [] : initialDataMap.get(child.type); - } - }); - } - - return defaultInitialData; -} diff --git a/src/form/utils/index.ts b/src/form/utils/index.ts index a83b352b84..c24b75ad73 100644 --- a/src/form/utils/index.ts +++ b/src/form/utils/index.ts @@ -1,3 +1,8 @@ +import has from 'lodash/has'; +import get from 'lodash/get'; +import isObject from 'lodash/isObject'; +import isArray from 'lodash/isArray'; +import isEmpty from 'lodash/isEmpty'; import type { NamePath } from '../type'; // 获取 formMap 管理的数据 @@ -12,6 +17,26 @@ export function getMapValue(name: NamePath, formMapRef: React.MutableRefObject [['user', 'name']] +// 不处理数组类型 +// { user: [{ name: '' }]} => [['user']] +export function objectToArray(obj: Record) { + const result: (string | number)[][] = []; + + function traverse(current: any, path: string[] = []) { + if (isObject(current) && !isArray(current) && !isEmpty(current)) { + Object.keys(current).forEach((key) => { + traverse(current[key], [...path, key]); + }); + } else { + result.push(path); + } + } + + traverse(obj); + return result; +} + // 将数据整理成对象,数组 name 转化嵌套对象: ['user', 'name'] => { user: { name: '' } } export function calcFieldValue(name: NamePath, value: any) { if (Array.isArray(name)) { @@ -26,36 +51,15 @@ export function calcFieldValue(name: NamePath, value: any) { return { [name]: value }; } -// 通过对象数据类型获取 map 引用: { user: { name: '' } } => formMap.get(['user', 'name']) +// // 通过对象数据类型获取 map 引用: { user: { name: '' } } => formMap.get(['user', 'name']) export function travelMapFromObject( obj: Record, formMapRef: React.MutableRefObject>, callback: Function, ) { for (const [mapName, formItemRef] of formMapRef.current.entries()) { - // 支持嵌套数据结构 - if (Array.isArray(mapName)) { - // 创建唯一临时变量 symbol - const symbol = Symbol('name'); - let fieldValue = null; - - for (let i = 0; i < mapName.length; i++) { - const item = mapName[i]; - if (Reflect.has(fieldValue || obj, item)) { - fieldValue = Reflect.get(fieldValue || obj, item); - } else { - // 当反射无法获取到值则重置为 symbol - fieldValue = symbol; - break; - } - } - - // 非 symbol 说明获取到了值 - if (fieldValue !== symbol) { - callback(formItemRef, fieldValue); - } - } else if (Reflect.has(obj, mapName)) { - callback(formItemRef, obj[mapName]); + if (has(obj, mapName)) { + callback(formItemRef, get(obj, mapName)); } } }