Skip to content

Commit fcd1650

Browse files
committed
fix: optimize logic
1 parent f5833f5 commit fcd1650

5 files changed

Lines changed: 110 additions & 188 deletions

File tree

‎packages/common‎

Submodule common updated 192 files

‎packages/components/form/Form.tsx‎

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
1-
import React, { useCallback, useEffect, useImperativeHandle, useRef } from 'react';
1+
import React, { useEffect, useImperativeHandle, useRef } from 'react';
22
import classNames from 'classnames';
33

44
import forwardRefWithStatics from '../_util/forwardRefWithStatics';
55
import noop from '../_util/noop';
66
import useConfig from '../hooks/useConfig';
77
import useDefaultProps from '../hooks/useDefaultProps';
8-
import useLayoutEffect from '../hooks/useLayoutEffect';
98
import { formDefaultProps } from './defaultProps';
109
import FormContext from './FormContext';
1110
import FormItem from './FormItem';
1211
import FormList from './FormList';
1312
import useForm, { HOOK_MARK } from './hooks/useForm';
1413
import useInstance from './hooks/useInstance';
14+
import useLastFormItem from './hooks/useLastFormItem';
1515
import useWatch from './hooks/useWatch';
16-
import { resolveLastFormItems } from './utils/lastFormItem';
1716

1817
import type { StyledProps } from '../common';
1918
import type { TdFormProps } from './type';
@@ -56,67 +55,11 @@ const Form = forwardRefWithStatics(
5655
const [form] = useForm(props.form); // 内部与外部共享 form 实例,外部不传则内部创建
5756
const formRef = useRef<HTMLFormElement>(null);
5857
const formMapRef = useRef(new Map()); // 收集所有包含 name 属性 formItem 实例
59-
const formItemElementsRef = useRef(new Set<HTMLElement>()); // 收集所有参与布局的 FormItem
60-
const lastFormItemsRef = useRef(new Set<HTMLElement>());
6158
const floatingFormDataRef = useRef({}); // 储存游离值的 formData
6259
const mountedFieldsRef = useRef<Set<string>>(new Set()); // Form 生命周期内所有曾经被 FormItem 挂载过的字段(fullPath 序列化后的字符串)
6360

6461
const formInstance = useInstance(props, formRef, formMapRef, floatingFormDataRef, form);
65-
66-
const refreshLastFormItem = useCallback(() => {
67-
const formElement = formRef.current;
68-
const lastClassName = `${classPrefix}-form__item--last`;
69-
const nextLastItems = new Set<HTMLElement>();
70-
71-
if (layout !== 'inline' && formElement) {
72-
resolveLastFormItems(formElement, formItemElementsRef.current, `${classPrefix}-form__item`).forEach((item) => {
73-
nextLastItems.add(item);
74-
});
75-
}
76-
77-
lastFormItemsRef.current.forEach((item) => {
78-
if (!nextLastItems.has(item)) item.classList.remove(lastClassName);
79-
});
80-
nextLastItems.forEach((item) => {
81-
if (!lastFormItemsRef.current.has(item)) item.classList.add(lastClassName);
82-
});
83-
lastFormItemsRef.current = nextLastItems;
84-
}, [classPrefix, layout]);
85-
86-
const registerFormItem = useCallback(
87-
(node: HTMLElement) => {
88-
formItemElementsRef.current.add(node);
89-
refreshLastFormItem();
90-
91-
return () => {
92-
formItemElementsRef.current.delete(node);
93-
node.classList.remove(`${classPrefix}-form__item--last`);
94-
lastFormItemsRef.current.delete(node);
95-
refreshLastFormItem();
96-
};
97-
},
98-
[classPrefix, refreshLastFormItem],
99-
);
100-
101-
useLayoutEffect(() => {
102-
const formElement = formRef.current;
103-
if (!formElement) return;
104-
105-
refreshLastFormItem();
106-
107-
const observer = new MutationObserver(refreshLastFormItem);
108-
observer.observe(formElement, { childList: true, subtree: true });
109-
110-
const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(refreshLastFormItem);
111-
resizeObserver?.observe(formElement);
112-
113-
return () => {
114-
observer.disconnect();
115-
resizeObserver?.disconnect();
116-
lastFormItemsRef.current.forEach((item) => item.classList.remove(`${classPrefix}-form__item--last`));
117-
lastFormItemsRef.current.clear();
118-
};
119-
}, [classPrefix, refreshLastFormItem]);
62+
const registerFormItem = useLastFormItem(formRef, layout, classPrefix);
12063

12164
useImperativeHandle(ref, () => formInstance);
12265
Object.assign(form, { ...formInstance });

‎packages/components/form/__tests__/form.test.tsx‎

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { fireEvent, mockDelay, mockTimeout, render, vi } from '@test/utils';
44

55
import Button from '../../button';
66
import Checkbox from '../../checkbox';
7+
import { Col, Row } from '../../grid';
78
import Input from '../../input';
89
import InputNumber from '../../input-number';
910
import Radio from '../../radio';
@@ -904,6 +905,7 @@ describe('Form', () => {
904905
<Button type="submit">submit</Button>
905906
</FormItem>
906907
);
908+
const contentBoxStyle = { paddingBottom: 16 };
907909

908910
const expectOnlyLastMarked = (container: HTMLElement) => {
909911
const formItems = getFormItems(container);
@@ -986,17 +988,20 @@ describe('Form', () => {
986988
expectNoneMarked(container);
987989
});
988990

989-
test('shared non-form parent marks both the group tail and the form tail', () => {
991+
test('layout wrappers do not mark a group tail', () => {
990992
const { container } = render(
991993
<Form>
992-
<div>
993-
<FormItem name="first">
994-
<Input />
995-
</FormItem>
996-
<FormItem name="second">
997-
<Input />
998-
</FormItem>
999-
</div>
994+
<div>{fields}</div>
995+
{actionItem}
996+
</Form>,
997+
);
998+
expectOnlyLastMarked(container);
999+
});
1000+
1001+
test('content box marks its last row when nothing follows inside', () => {
1002+
const { container } = render(
1003+
<Form>
1004+
<div style={contentBoxStyle}>{fields}</div>
10001005
{actionItem}
10011006
</Form>,
10021007
);
@@ -1006,6 +1011,63 @@ describe('Form', () => {
10061011
expect(formItems[2]).toHaveClass('t-form__item--last');
10071012
});
10081013

1014+
test('content box keeps spacing when actions stay inside', () => {
1015+
const { container } = render(
1016+
<Form>
1017+
<div style={contentBoxStyle}>
1018+
{fields}
1019+
<Button type="submit">submit</Button>
1020+
</div>
1021+
</Form>,
1022+
);
1023+
expectNoneMarked(container);
1024+
});
1025+
1026+
test('Col wrapping does not mark a group tail', () => {
1027+
const { container } = render(
1028+
<Form>
1029+
<Row>
1030+
<Col span={6}>{fields}</Col>
1031+
<Col span={6}>
1032+
<FormItem name="third">
1033+
<Input />
1034+
</FormItem>
1035+
</Col>
1036+
</Row>
1037+
<Button type="submit">submit</Button>
1038+
</Form>,
1039+
);
1040+
expectNoneMarked(container);
1041+
});
1042+
1043+
test('validate re-render keeps --last on the content box tail', async () => {
1044+
const Demo = () => {
1045+
const [form] = Form.useForm();
1046+
useEffect(() => {
1047+
form.validate();
1048+
}, [form]);
1049+
return (
1050+
<Form
1051+
form={form}
1052+
rules={{
1053+
first: [{ required: true, message: 'required', type: 'error' }],
1054+
second: [{ required: true, message: 'required', type: 'error' }],
1055+
}}
1056+
>
1057+
<div style={contentBoxStyle}>{fields}</div>
1058+
{actionItem}
1059+
</Form>
1060+
);
1061+
};
1062+
1063+
const { container } = render(<Demo />);
1064+
await mockDelay();
1065+
const formItems = getFormItems(container);
1066+
expect(formItems[1]).toHaveClass('t-form__item--last');
1067+
expect(formItems[1]).toHaveClass('t-form__item-with-extra');
1068+
expect(formItems[2]).toHaveClass('t-form__item--last');
1069+
});
1070+
10091071
test('hidden last FormItem does not occupy last-item slot', () => {
10101072
const { container } = render(
10111073
<Form>
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { useCallback, useRef } from 'react';
2+
import { LastFormItemController } from '@tdesign/common-js/form/last-form-item';
3+
4+
import useLayoutEffect from '../../hooks/useLayoutEffect';
5+
6+
import type { RefObject } from 'react';
7+
import type { TdFormProps } from '../type';
8+
9+
export default function useLastFormItem(
10+
formRef: RefObject<HTMLFormElement>,
11+
layout: TdFormProps['layout'],
12+
classPrefix: string,
13+
) {
14+
const controllerRef = useRef<LastFormItemController | null>(null);
15+
16+
if (!controllerRef.current) {
17+
controllerRef.current = new LastFormItemController({
18+
classPrefix,
19+
disabled: layout === 'inline',
20+
});
21+
}
22+
const controller = controllerRef.current;
23+
24+
const registerFormItem = useCallback((node: HTMLElement) => controller.register(node), [controller]);
25+
26+
useLayoutEffect(() => {
27+
controller.update({ classPrefix, disabled: layout === 'inline' });
28+
controller.bind(formRef.current);
29+
return () => controller.unbind();
30+
}, [classPrefix, controller, formRef, layout]);
31+
32+
useLayoutEffect(() => () => controller.destroy(), [controller]);
33+
34+
return registerFormItem;
35+
}

‎packages/components/form/utils/lastFormItem.ts‎

Lines changed: 0 additions & 118 deletions
This file was deleted.

0 commit comments

Comments
 (0)