From ceebac623acec82ffb6a3a56a903e216daef0564 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Mon, 14 Oct 2024 12:21:05 +0800 Subject: [PATCH 01/10] chore(form): fixed variable naming misspellings --- src/form/FormItem.tsx | 6 +++--- src/form/useInitialData.ts | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/form/FormItem.tsx b/src/form/FormItem.tsx index 4568fef944..2fd1d3cfe2 100644 --- a/src/form/FormItem.tsx +++ b/src/form/FormItem.tsx @@ -62,7 +62,7 @@ const FormItem = forwardRef((originalProps, ref form, colon, layout, - initialData: FromContextInitialData, + initialData: FormContextInitialData, requiredMark: requiredMarkFromContext, labelAlign: labelAlignFromContext, labelWidth: labelWidthFromContext, @@ -118,7 +118,7 @@ const FormItem = forwardRef((originalProps, ref formListName, children, initialData, - FromContextInitialData, + FormContextInitialData, FormListInitialData, }), ); @@ -329,7 +329,7 @@ const FormItem = forwardRef((originalProps, ref formListName, children, initialData, - FromContextInitialData, + FormContextInitialData, FormListInitialData, }); } diff --git a/src/form/useInitialData.ts b/src/form/useInitialData.ts index 5fb821a7ca..bae908c31b 100644 --- a/src/form/useInitialData.ts +++ b/src/form/useInitialData.ts @@ -34,15 +34,15 @@ export function getDefaultInitialData({ formListName, children, initialData, - FromContextInitialData, + FormContextInitialData, FormListInitialData, }) { let defaultInitialData; - if (FromContextInitialData) { - if (typeof name === 'string') defaultInitialData = get(FromContextInitialData, name); + if (FormContextInitialData) { + if (typeof name === 'string') defaultInitialData = get(FormContextInitialData, name); if (Array.isArray(name)) { const nameList = formListName ? [formListName, ...name] : name; - defaultInitialData = get(FromContextInitialData, nameList); + defaultInitialData = get(FormContextInitialData, nameList); } } if (FormListInitialData.length) { From 581ddb5a127fe3ed685e659f76367582d90c84fc Mon Sep 17 00:00:00 2001 From: Elvis Liao Date: Wed, 16 Oct 2024 02:28:25 +0800 Subject: [PATCH 02/10] chore(form): fixed type issues --- src/form/FormContext.tsx | 2 +- src/form/FormItem.tsx | 2 +- src/form/useInitialData.ts | 31 +++++++++++++++++++++++-------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/src/form/FormContext.tsx b/src/form/FormContext.tsx index d9027b7bb0..f564046496 100644 --- a/src/form/FormContext.tsx +++ b/src/form/FormContext.tsx @@ -47,7 +47,7 @@ export const FormListContext = React.createContext<{ name: NamePath; rules: TdFormListProps['rules']; formListMapRef: React.RefObject>>; - initialData: TdFormProps['initialData']; + initialData: TdFormListProps['initialData']; }>({ name: undefined, rules: undefined, diff --git a/src/form/FormItem.tsx b/src/form/FormItem.tsx index 2fd1d3cfe2..88194429d8 100644 --- a/src/form/FormItem.tsx +++ b/src/form/FormItem.tsx @@ -32,7 +32,7 @@ import useDefaultProps from '../hooks/useDefaultProps'; import { useLocaleReceiver } from '../locale/LocalReceiver'; export interface FormItemProps extends TdFormItemProps, StyledProps { - children?: React.ReactNode | ((form: FormInstanceFunctions) => React.ReactElement); + children?: React.ReactNode | React.ReactNode[] | ((form: FormInstanceFunctions) => React.ReactElement); } export interface FormItemInstance { diff --git a/src/form/useInitialData.ts b/src/form/useInitialData.ts index bae908c31b..e1e025eea6 100644 --- a/src/form/useInitialData.ts +++ b/src/form/useInitialData.ts @@ -1,5 +1,6 @@ import React from 'react'; import get from 'lodash/get'; +import isFunction from 'lodash/isFunction'; // 兼容特殊数据结构和受控 key import Tree from '../tree/Tree'; @@ -13,6 +14,9 @@ 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'); @@ -36,12 +40,19 @@ export function getDefaultInitialData({ 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] : name; + const nameList = formListName ? [formListName, name].flat() : name; defaultInitialData = get(FormContextInitialData, nameList); } } @@ -49,12 +60,16 @@ export function getDefaultInitialData({ defaultInitialData = get(FormListInitialData, name); } if (typeof initialData !== 'undefined') defaultInitialData = initialData; - 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); - } - }); + + 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; } From 63698fb3f1802469ea3f13dc6a0c5012d4fe6cbf Mon Sep 17 00:00:00 2001 From: Elvis Liao Date: Wed, 16 Oct 2024 02:44:48 +0800 Subject: [PATCH 03/10] 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)); } } } From 7d6089a177c963d207e43bb708a3012afd500cc2 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Thu, 17 Oct 2024 11:47:45 +0800 Subject: [PATCH 04/10] test(form): add a test case for issue #3076 --- src/form/__tests__/form.test.tsx | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/form/__tests__/form.test.tsx b/src/form/__tests__/form.test.tsx index 01be9666e7..4e3c277427 100644 --- a/src/form/__tests__/form.test.tsx +++ b/src/form/__tests__/form.test.tsx @@ -5,6 +5,7 @@ import React, { useEffect, useState } from 'react'; import Form, { TdFormProps } from '../index'; import Input from '../../input'; import Button from '../../button'; +import Radio from '../../radio'; import { HelpCircleIcon } from 'tdesign-icons-react'; const { FormItem } = Form; @@ -341,4 +342,60 @@ describe('Form 组件测试', () => { await mockDelay(); expect(container.querySelector('.t-input__extra').innerHTML).toBe('please input username'); }); + + test('动态渲染并初始赋值', () => { + const TestForm = () => { + const [form] = Form.useForm(); + const setMessage = () => { + form.setFieldsValue({ + gender: 'female', + radio2: '3', + }); + }; + + return ( +
+ + + 男性 + 女性 + + + prev.gender !== next.gender}> + {({ getFieldValue }) => { + if (getFieldValue('gender') === 'female') { + return ( + + + 选项三 + + 选项四 + + + + ); + } + return ( + + + 选项一 + 选项二 + + + ); + }} + + + + + +
+ ); + }; + + const { container, getByText } = render(); + fireEvent.click(getByText('设置信息')); + + expect(container.querySelector('.radio-value-3')).toHaveClass('t-is-checked'); + }); }); From f7b50158d4e72d37446c126ce35bb3d26fa7fd41 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Thu, 17 Oct 2024 13:29:15 +0800 Subject: [PATCH 05/10] fix(FormItem): fixed formItem default value --- src/form/hooks/useFormItemInitialData.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/form/hooks/useFormItemInitialData.ts b/src/form/hooks/useFormItemInitialData.ts index 20d5f81288..823e68abba 100644 --- a/src/form/hooks/useFormItemInitialData.ts +++ b/src/form/hooks/useFormItemInitialData.ts @@ -53,7 +53,7 @@ export default function useFormItemInitialData() { if (typeof defaultInitialData !== 'undefined') return defaultInitialData; } - if (initialData) { + if (typeof initialData !== 'undefined') { return initialData; } From f8c9abcbcb5a46ece66d5e1b9842bd643853e799 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Fri, 18 Oct 2024 14:02:50 +0800 Subject: [PATCH 06/10] =?UTF-8?q?chore(Form):=20=E8=B0=83=E6=95=B4=20float?= =?UTF-8?q?ingFormData=20=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 floatingFormDataRef 移至 formContext - 将 FormItem 渲染后删除对应游离值的逻辑移至 userFormItemInitialData hook 内 --- src/form/Form.tsx | 12 +++------- src/form/FormContext.tsx | 2 ++ src/form/FormItem.tsx | 12 ++-------- src/form/hooks/useFormItemInitialData.ts | 30 +++++++++++++++++------- src/form/hooks/useInstance.tsx | 16 +++++-------- src/form/type.ts | 8 ------- src/form/utils/index.ts | 2 +- 7 files changed, 35 insertions(+), 47 deletions(-) diff --git a/src/form/Form.tsx b/src/form/Form.tsx index cfebc80d51..8163e3a642 100644 --- a/src/form/Form.tsx +++ b/src/form/Form.tsx @@ -52,7 +52,8 @@ const Form = forwardRefWithStatics( const [form] = useForm(props.form); // 内部与外部共享 form 实例,外部不传则内部创建 const formRef = useRef(); const formMapRef = useRef(new Map()); // 收集所有包含 name 属性 formItem 实例 - const formInstance = useInstance(props, formRef, formMapRef); + const floatingFormDataRef = useRef({}); // 储存游离值的 formData + const formInstance = useInstance(props, formRef, formMapRef, floatingFormDataRef); useImperativeHandle(ref, () => formInstance); Object.assign(form, { ...formInstance }); @@ -63,14 +64,6 @@ 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(); @@ -111,6 +104,7 @@ const Form = forwardRefWithStatics( rules, disabled, formMapRef, + floatingFormDataRef, onFormItemValueChange, }} > diff --git a/src/form/FormContext.tsx b/src/form/FormContext.tsx index f564046496..608033526d 100644 --- a/src/form/FormContext.tsx +++ b/src/form/FormContext.tsx @@ -19,6 +19,7 @@ const FormContext = React.createContext<{ rules: TdFormProps['rules']; errorMessage: TdFormProps['errorMessage']; formMapRef: React.RefObject>>; + floatingFormDataRef: React.RefObject>; onFormItemValueChange: (changedValue: Record) => void; }>({ form: undefined, @@ -37,6 +38,7 @@ const FormContext = React.createContext<{ statusIcon: undefined, onFormItemValueChange: undefined, formMapRef: undefined, + floatingFormDataRef: undefined, }); export const useFormContext = () => React.useContext(FormContext); diff --git a/src/form/FormItem.tsx b/src/form/FormItem.tsx index beabb1b5c8..9151499b53 100644 --- a/src/form/FormItem.tsx +++ b/src/form/FormItem.tsx @@ -2,7 +2,6 @@ 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 { @@ -78,8 +77,6 @@ const FormItem = forwardRef((originalProps, ref const { name: formListName, rules: formListRules, formListMapRef } = useFormListContext(); - const { getDefaultInitialData } = useFormItemInitialData(); - const props = useDefaultProps(originalProps, formItemDefaultProps); const { @@ -102,6 +99,8 @@ const FormItem = forwardRef((originalProps, ref requiredMark = requiredMarkFromContext, } = props; + const { getDefaultInitialData } = useFormItemInitialData(name); + const [, forceUpdate] = useState({}); // custom render state const [freeShowErrorMessage, setFreeShowErrorMessage] = useState(undefined); const [errorList, setErrorList] = useState([]); @@ -111,16 +110,10 @@ const FormItem = forwardRef((originalProps, ref const [needResetField, setNeedResetField] = useState(false); const [formValue, setFormValue] = useState( getDefaultInitialData({ - name, children, initialData, }), ); - // 组件渲染后删除对应游离值 - useEffect(() => { - const nameList = formListName ? [formListName, name].flat() : name; - unset(form.floatingFormData, nameList); - }, [form.floatingFormData, formListName, name]); const formItemRef = useRef(); // 当前 formItem 实例 const innerFormItemsRef = useRef([]); @@ -324,7 +317,6 @@ const FormItem = forwardRef((originalProps, ref function getResetValue(resetType: string): ValueType { if (resetType === 'initial') { return getDefaultInitialData({ - name, children, initialData, }); diff --git a/src/form/hooks/useFormItemInitialData.ts b/src/form/hooks/useFormItemInitialData.ts index 823e68abba..2e8908314c 100644 --- a/src/form/hooks/useFormItemInitialData.ts +++ b/src/form/hooks/useFormItemInitialData.ts @@ -1,5 +1,7 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import get from 'lodash/get'; +import unset from 'lodash/unset'; +import isEmpty from 'lodash/isEmpty'; // 兼容特殊数据结构和受控 key import Tree from '../../tree/Tree'; @@ -31,26 +33,36 @@ export const initialDataMap = new Map(); initialDataMap.set(component, false); }); -export default function useFormItemInitialData() { - const { form, initialData: formContextInitialData } = useFormContext(); - const { floatingFormData } = form; +export default function useFormItemInitialData(name: FormItemProps['name']) { + let hadReadFloatingFormData = false; + + const { floatingFormDataRef, initialData: formContextInitialData } = useFormContext(); const { name: formListName, initialData: formListInitialData } = useFormListContext(); + // 组件渲染后删除对应游离值 + useEffect(() => { + if (hadReadFloatingFormData) { + const nameList = formListName ? [formListName, name].flat() : name; + unset(floatingFormDataRef.current, nameList); + } + }, [hadReadFloatingFormData, floatingFormDataRef, formListName, name]); + // 整理初始值 优先级:Form.initialData < FormList.initialData < FormItem.initialData < floatFormData function getDefaultInitialData({ - name, children, initialData, }: { - name: FormItemProps['name']; children: FormItemProps['children']; initialData: FormItemProps['initialData']; }) { - if (name && floatingFormData) { + if (name && !isEmpty(floatingFormDataRef.current)) { const nameList = formListName ? [formListName, name].flat() : name; - const defaultInitialData = get(floatingFormData, nameList); - if (typeof defaultInitialData !== 'undefined') return defaultInitialData; + const defaultInitialData = get(floatingFormDataRef.current, nameList); + if (typeof defaultInitialData !== 'undefined') { + hadReadFloatingFormData = true; + return defaultInitialData; + } } if (typeof initialData !== 'undefined') { diff --git a/src/form/hooks/useInstance.tsx b/src/form/hooks/useInstance.tsx index 8ab931dd57..cd4be88d1f 100644 --- a/src/form/hooks/useInstance.tsx +++ b/src/form/hooks/useInstance.tsx @@ -3,7 +3,6 @@ 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, @@ -43,9 +42,13 @@ function formatValidateResult(validateResultList) { return isEmpty(result) ? true : result; } -export default function useInstance(props: TdFormProps, formRef, formMapRef: React.MutableRefObject>) { +export default function useInstance( + props: TdFormProps, + formRef, + formMapRef: React.MutableRefObject>, + floatingFormDataRef: React.RefObject>, +) { const { classPrefix } = useConfig(); - const floatingFormDataRef = useRef>({}); const { scrollToFirstError, preventSubmitDefault = true, onSubmit, onReset } = props; @@ -210,11 +213,6 @@ export default function useInstance(props: TdFormProps, formRef, formMapRef: Rea }); } - // 对外方法,清空 floatingFormData - function clearFloatingFormData() { - floatingFormDataRef.current = {}; - } - return { submit, reset, @@ -228,7 +226,5 @@ 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 fee1abf2f2..909bfb6cc3 100644 --- a/src/form/type.ts +++ b/src/form/type.ts @@ -153,14 +153,6 @@ 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/utils/index.ts b/src/form/utils/index.ts index c24b75ad73..da0d54e38e 100644 --- a/src/form/utils/index.ts +++ b/src/form/utils/index.ts @@ -51,7 +51,7 @@ 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>, From b4afb583adfb2d472e94776bc170648ebc5ba3b9 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Fri, 18 Oct 2024 14:13:37 +0800 Subject: [PATCH 07/10] =?UTF-8?q?docs(Form):=20=E5=B0=86=20FQA=20=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=20=E3=80=90=E5=A6=82=E4=BD=95=E6=A0=B9=E6=8D=AE?= =?UTF-8?q?=E6=9F=90=E4=B8=AA=E5=AD=97=E6=AE=B5=E5=8F=98=E5=8C=96=E5=8A=A8?= =?UTF-8?q?=E6=80=81=E5=B1=95=E7=A4=BA=E6=95=B0=E6=8D=AE=E3=80=91=20?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=20=E3=80=90=E5=AD=97=E6=AE=B5=E8=81=94?= =?UTF-8?q?=E5=8A=A8=E7=9A=84=E8=A1=A8=E5=8D=95=E7=A4=BA=E4=BE=8B=E3=80=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/form/_example/form-field-linkage.tsx | 47 ++++++++++++++++++++++++ src/form/form.md | 26 +++---------- 2 files changed, 53 insertions(+), 20 deletions(-) create mode 100644 src/form/_example/form-field-linkage.tsx diff --git a/src/form/_example/form-field-linkage.tsx b/src/form/_example/form-field-linkage.tsx new file mode 100644 index 0000000000..69f7790e74 --- /dev/null +++ b/src/form/_example/form-field-linkage.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import Form from '../Form'; +import Radio from '../../radio'; +import Button from '../../button'; + +const { FormItem } = Form; + +export default function FormExample() { + const [form] = Form.useForm(); + const setMessage = () => { + form.setFieldsValue({ + type: 'cold', + ice: '1', + }); + }; + + return ( +
+ + + 热饮 + 冷饮 + + + prev.type !== next.type}> + {({ getFieldValue }) => { + if (getFieldValue('type') === 'cold') { + return ( + + + 正常冰 + 少冰 + 去冰 + + + ); + } + return null; + }} + + + + + +
+ ); +} diff --git a/src/form/form.md b/src/form/form.md index 09fc055486..5f5be5414a 100644 --- a/src/form/form.md +++ b/src/form/form.md @@ -12,6 +12,12 @@ {{ form-list }} +### 字段联动的表单 + +在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 `FormItem` 的更新逻辑。 + +{{ form-field-linkage }} + ### 自定义表单控件 可以使用 `Form.FormItem` 包裹自定义组件并在组件中接受 `value` 和 `onChange` 的入参,实现自定义表单控件。 @@ -76,26 +82,6 @@ Form 组件设计的初衷是为了解放开发者配置大量的 `value`、`onC ``` -### 如何根据某个字段变化动态展示数据 - -而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 `FormItem` 的更新逻辑。 - -```js - prev.additional !== next.additional}> - {({ getFieldValue }) => { - if (getFieldValue('additional') === 'test') { - return ( - - - - ); - } - return null; - }} - - -``` - ## API ### Form Props From b7ad2f367ffe91bcf7c3332a4d74282478a1a342 Mon Sep 17 00:00:00 2001 From: elvis liao <1219585136@qq.com> Date: Fri, 18 Oct 2024 14:45:27 +0800 Subject: [PATCH 08/10] chore(Form): update test snapshots --- test/snap/__snapshots__/csr.test.jsx.snap | 101 ++++++++++++++++++++++ test/snap/__snapshots__/ssr.test.jsx.snap | 2 + 2 files changed, 103 insertions(+) diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index c151a340b7..c0eaa4505f 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -44223,6 +44223,105 @@ exports[`csr snapshot test > csr test src/form/_example/error-message.tsx 1`] = `; +exports[`csr snapshot test > csr test src/form/_example/form-field-linkage.tsx 1`] = ` +
+
+
+
+ + : +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+
+`; + exports[`csr snapshot test > csr test src/form/_example/form-list.tsx 1`] = `
ssr test src/form/_example/disabled.tsx 1`] = `" ssr test src/form/_example/error-message.tsx 1`] = `"
这是用户名字段帮助说明
一句话介绍自己
"`; +exports[`ssr snapshot test > ssr test src/form/_example/form-field-linkage.tsx 1`] = `"
:
"`; + exports[`ssr snapshot test > ssr test src/form/_example/form-list.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/form/_example/layout.tsx 1`] = `"
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index d1576ce4f7..c23618bf87 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -412,6 +412,8 @@ exports[`ssr snapshot test > ssr test src/form/_example/disabled.tsx 1`] = `" ssr test src/form/_example/error-message.tsx 1`] = `"
这是用户名字段帮助说明
一句话介绍自己
"`; +exports[`ssr snapshot test > ssr test src/form/_example/form-field-linkage.tsx 1`] = `"
:
"`; + exports[`ssr snapshot test > ssr test src/form/_example/form-list.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/form/_example/layout.tsx 1`] = `"
"`; From c3dc8c4a8b76c9b99ac017edafa8ce3893066a5f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 18 Oct 2024 07:30:48 +0000 Subject: [PATCH 09/10] chore: update snapshot --- test/snap/__snapshots__/csr.test.jsx.snap | 101 ++++++++++++++++++++++ test/snap/__snapshots__/ssr.test.jsx.snap | 2 + 2 files changed, 103 insertions(+) diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index 057b6fd6c7..32ecd6c11e 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -44223,6 +44223,105 @@ exports[`csr snapshot test > csr test src/form/_example/error-message.tsx 1`] =
`; +exports[`csr snapshot test > csr test src/form/_example/form-field-linkage.tsx 1`] = ` +
+
+
+
+ + : +
+
+
+
+ + +
+
+
+
+
+
+
+ +
+
+
+
+
+`; + exports[`csr snapshot test > csr test src/form/_example/form-list.tsx 1`] = `
ssr test src/form/_example/disabled.tsx 1`] = `" ssr test src/form/_example/error-message.tsx 1`] = `"
一句话介绍自己
"`; +exports[`ssr snapshot test > ssr test src/form/_example/form-field-linkage.tsx 1`] = `"
:
"`; + exports[`ssr snapshot test > ssr test src/form/_example/form-list.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/form/_example/layout.tsx 1`] = `"
"`; diff --git a/test/snap/__snapshots__/ssr.test.jsx.snap b/test/snap/__snapshots__/ssr.test.jsx.snap index bf22b6d3f0..152daaa315 100644 --- a/test/snap/__snapshots__/ssr.test.jsx.snap +++ b/test/snap/__snapshots__/ssr.test.jsx.snap @@ -412,6 +412,8 @@ exports[`ssr snapshot test > ssr test src/form/_example/disabled.tsx 1`] = `" ssr test src/form/_example/error-message.tsx 1`] = `"
一句话介绍自己
"`; +exports[`ssr snapshot test > ssr test src/form/_example/form-field-linkage.tsx 1`] = `"
:
"`; + exports[`ssr snapshot test > ssr test src/form/_example/form-list.tsx 1`] = `"
"`; exports[`ssr snapshot test > ssr test src/form/_example/layout.tsx 1`] = `"
"`; From 4964097bc86bd7cf9958e17723a300c57a51be84 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 19 Oct 2024 14:45:56 +0000 Subject: [PATCH 10/10] chore: update common --- src/_common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_common b/src/_common index ddf8006355..4bdf9ced2d 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit ddf8006355ee1a42a91b7c10f090389071bc7eaf +Subproject commit 4bdf9ced2d0b1c3aaf88c035424ef2230d71f396