Skip to content

Commit

Permalink
fix(form): fixed dynamic rendering of form default data (#3145)
Browse files Browse the repository at this point in the history
* chore(form): fixed variable naming misspellings

* chore(form): fixed type issues

* fix(Form): fixed issue #3076

* test(form): add a test case for issue #3076

* fix(FormItem): fixed formItem default value

* chore(Form): 调整 floatingFormData 逻辑

 - 将 floatingFormDataRef 移至 formContext
 - 将 FormItem 渲染后删除对应游离值的逻辑移至 userFormItemInitialData hook 内

* docs(Form): 将 FQA 中的 【如何根据某个字段变化动态展示数据】 改为 【字段联动的表单示例】

* chore(Form): update test snapshots

* chore: update snapshot

* chore: update common

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
l123wx and github-actions[bot] authored Oct 31, 2024
1 parent 778a2f7 commit 476dd87
Show file tree
Hide file tree
Showing 13 changed files with 367 additions and 127 deletions.
2 changes: 1 addition & 1 deletion src/_common
4 changes: 3 additions & 1 deletion src/form/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ const Form = forwardRefWithStatics(
const [form] = useForm(props.form); // 内部与外部共享 form 实例,外部不传则内部创建
const formRef = useRef<HTMLFormElement>();
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 });
Expand Down Expand Up @@ -103,6 +104,7 @@ const Form = forwardRefWithStatics(
rules,
disabled,
formMapRef,
floatingFormDataRef,
onFormItemValueChange,
}}
>
Expand Down
4 changes: 3 additions & 1 deletion src/form/FormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const FormContext = React.createContext<{
rules: TdFormProps['rules'];
errorMessage: TdFormProps['errorMessage'];
formMapRef: React.RefObject<Map<any, React.RefObject<FormItemInstance>>>;
floatingFormDataRef: React.RefObject<Record<any, any>>;
onFormItemValueChange: (changedValue: Record<string, unknown>) => void;
}>({
form: undefined,
Expand All @@ -37,6 +38,7 @@ const FormContext = React.createContext<{
statusIcon: undefined,
onFormItemValueChange: undefined,
formMapRef: undefined,
floatingFormDataRef: undefined,
});

export const useFormContext = () => React.useContext(FormContext);
Expand All @@ -47,7 +49,7 @@ export const FormListContext = React.createContext<{
name: NamePath;
rules: TdFormListProps['rules'];
formListMapRef: React.RefObject<Map<any, React.RefObject<FormItemInstance>>>;
initialData: TdFormProps['initialData'];
initialData: TdFormListProps['initialData'];
}>({
name: undefined,
rules: undefined,
Expand Down
22 changes: 5 additions & 17 deletions src/form/FormItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ 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';

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 {
Expand Down Expand Up @@ -62,7 +62,6 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
form,
colon,
layout,
initialData: FromContextInitialData,
requiredMark: requiredMarkFromContext,
labelAlign: labelAlignFromContext,
labelWidth: labelWidthFromContext,
Expand All @@ -76,12 +75,7 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
onFormItemValueChange,
} = useFormContext();

const {
name: formListName,
rules: formListRules,
formListMapRef,
initialData: FormListInitialData,
} = useFormListContext();
const { name: formListName, rules: formListRules, formListMapRef } = useFormListContext();

const props = useDefaultProps<FormItemProps>(originalProps, formItemDefaultProps);

Expand All @@ -105,6 +99,8 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
requiredMark = requiredMarkFromContext,
} = props;

const { getDefaultInitialData } = useFormItemInitialData(name);

const [, forceUpdate] = useState({}); // custom render state
const [freeShowErrorMessage, setFreeShowErrorMessage] = useState(undefined);
const [errorList, setErrorList] = useState([]);
Expand All @@ -114,12 +110,8 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
const [needResetField, setNeedResetField] = useState(false);
const [formValue, setFormValue] = useState(
getDefaultInitialData({
name,
formListName,
children,
initialData,
FromContextInitialData,
FormListInitialData,
}),
);

Expand Down Expand Up @@ -325,12 +317,8 @@ const FormItem = forwardRef<FormItemInstance, FormItemProps>((originalProps, ref
function getResetValue(resetType: string): ValueType {
if (resetType === 'initial') {
return getDefaultInitialData({
name,
formListName,
children,
initialData,
FromContextInitialData,
FormListInitialData,
});
}

Expand Down
57 changes: 57 additions & 0 deletions src/form/__tests__/form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 (
<Form form={form} colon labelWidth={100}>
<FormItem label="性别" name="gender" initialData="male">
<Radio.Group>
<Radio value="male">男性</Radio>
<Radio value="female">女性</Radio>
</Radio.Group>
</FormItem>
<FormItem shouldUpdate={(prev, next) => prev.gender !== next.gender}>
{({ getFieldValue }) => {
if (getFieldValue('gender') === 'female') {
return (
<FormItem label="动态选项2" key="radio2" name="radio2">
<Radio.Group className="radio-group-2">
<Radio value="2">选项三</Radio>
<Radio value="3" className="radio-value-3">
选项四
</Radio>
</Radio.Group>
</FormItem>
);
}
return (
<FormItem label="动态选项1" key="radio1" name="radio1" initialData="0">
<Radio.Group>
<Radio value="0">选项一</Radio>
<Radio value="1">选项二</Radio>
</Radio.Group>
</FormItem>
);
}}
</FormItem>

<FormItem style={{ marginLeft: 100 }}>
<Button onClick={setMessage}>设置信息</Button>
</FormItem>
</Form>
);
};

const { container, getByText } = render(<TestForm />);
fireEvent.click(getByText('设置信息'));

expect(container.querySelector('.radio-value-3')).toHaveClass('t-is-checked');
});
});
47 changes: 47 additions & 0 deletions src/form/_example/form-field-linkage.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Form form={form} colon labelWidth={100}>
<FormItem label="类型" name="type" initialData="hot">
<Radio.Group>
<Radio value="hot">热饮</Radio>
<Radio value="cold">冷饮</Radio>
</Radio.Group>
</FormItem>
<FormItem shouldUpdate={(prev, next) => prev.type !== next.type}>
{({ getFieldValue }) => {
if (getFieldValue('type') === 'cold') {
return (
<FormItem label="冰量" key="ice" name="ice">
<Radio.Group>
<Radio value="0">正常冰</Radio>
<Radio value="1">少冰</Radio>
<Radio value="2">去冰</Radio>
</Radio.Group>
</FormItem>
);
}
return null;
}}
</FormItem>

<FormItem style={{ marginLeft: 100 }}>
<Button onClick={setMessage}>选择冷饮-少冰</Button>
</FormItem>
</Form>
);
}
26 changes: 6 additions & 20 deletions src/form/form.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

{{ form-list }}

### 字段联动的表单

在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 `FormItem` 的更新逻辑。

{{ form-field-linkage }}

### 自定义表单控件

可以使用 `Form.FormItem` 包裹自定义组件并在组件中接受 `value``onChange` 的入参,实现自定义表单控件。
Expand Down Expand Up @@ -76,26 +82,6 @@ Form 组件设计的初衷是为了解放开发者配置大量的 `value`、`onC
</Form.FormItem>
```

### 如何根据某个字段变化动态展示数据

而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 `shouldUpdate` 修改 `FormItem` 的更新逻辑。

```js
<Form.FormItem shouldUpdate={(prev, next) => prev.additional !== next.additional}>
{({ getFieldValue }) => {
if (getFieldValue('additional') === 'test') {
return (
<Form.FormItem name="test">
<Input />
</Form.FormItem>
);
}
return null;
}}
<Input />
</Form.FormItem>
```

## API

### Form Props
Expand Down
96 changes: 96 additions & 0 deletions src/form/hooks/useFormItemInitialData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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';
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(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({
children,
initialData,
}: {
children: FormItemProps['children'];
initialData: FormItemProps['initialData'];
}) {
if (name && !isEmpty(floatingFormDataRef.current)) {
const nameList = formListName ? [formListName, name].flat() : name;
const defaultInitialData = get(floatingFormDataRef.current, nameList);
if (typeof defaultInitialData !== 'undefined') {
hadReadFloatingFormData = true;
return defaultInitialData;
}
}

if (typeof initialData !== 'undefined') {
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,
};
}
Loading

0 comments on commit 476dd87

Please sign in to comment.