@rainbow-oh/yee-c 0.14.4 → 0.14.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rainbow-oh/yee-c",
3
- "version": "0.14.4",
3
+ "version": "0.14.6",
4
4
  "description": "yee basic components",
5
5
  "keywords": [
6
6
  "react",
@@ -0,0 +1,126 @@
1
+ // @vitest-environment jsdom
2
+ import React from 'react';
3
+ import { cleanup, render } from '@testing-library/react';
4
+ import { act } from 'react';
5
+ import { afterEach, describe, expect, it } from 'vitest';
6
+
7
+ // act 需要(仓库无全局 vitest setup,逐文件声明)
8
+ (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
9
+ import Field from './Field';
10
+ import FieldContext from './FieldContext';
11
+ import List from './List';
12
+ import { FormStore } from './form-store';
13
+
14
+ function renderListWithStore() {
15
+ const store = new FormStore().getForm();
16
+ const { getByTestId } = render(
17
+ <FieldContext.Provider value={store}>
18
+ <List name="options">
19
+ {(fields) => (
20
+ <div data-testid="rows">
21
+ {fields.map((field) => (
22
+ <Field key={field.key} name={[field.name, 'label']}>
23
+ <input />
24
+ </Field>
25
+ ))}
26
+ </div>
27
+ )}
28
+ </List>
29
+ </FieldContext.Provider>,
30
+ );
31
+ return { getByTestId, store };
32
+ }
33
+
34
+ afterEach(() => {
35
+ cleanup();
36
+ });
37
+
38
+ // List 只在自身 render 时读 store:外部 form.setFieldsValue 整体替换 List 值时
39
+ // 若不订阅自身 path,行数不刷新、新行永不挂载(app 侧 issue #99 首开不显示的根因)
40
+ describe('Form.List 外部 setFieldsValue 驱动重渲染', () => {
41
+ it('整体替换 List 值后行数更新', () => {
42
+ const { getByTestId, store } = renderListWithStore();
43
+ expect(getByTestId('rows').children.length).toBe(0);
44
+
45
+ act(() => {
46
+ store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }, { label: 'c' }] });
47
+ });
48
+ expect(getByTestId('rows').children.length).toBe(3);
49
+ });
50
+
51
+ it('缩减 List 值后行数同步减少', () => {
52
+ const { getByTestId, store } = renderListWithStore();
53
+ act(() => {
54
+ store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }] });
55
+ });
56
+ expect(getByTestId('rows').children.length).toBe(2);
57
+
58
+ act(() => {
59
+ store.setFieldsValue({ options: [{ label: 'a' }] });
60
+ });
61
+ expect(getByTestId('rows').children.length).toBe(1);
62
+ });
63
+
64
+ it('resetFields 后行数清零', () => {
65
+ const { getByTestId, store } = renderListWithStore();
66
+ act(() => {
67
+ store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }] });
68
+ });
69
+ expect(getByTestId('rows').children.length).toBe(2);
70
+
71
+ act(() => {
72
+ store.resetFields();
73
+ });
74
+ expect(getByTestId('rows').children.length).toBe(0);
75
+ });
76
+
77
+ it('行内编辑不重渲染 List(长度未变时跳过)', () => {
78
+ let renders = 0;
79
+ const store = new FormStore().getForm();
80
+ render(
81
+ <FieldContext.Provider value={store}>
82
+ <List name="options">
83
+ {() => {
84
+ renders += 1;
85
+ return null;
86
+ }}
87
+ </List>
88
+ </FieldContext.Provider>,
89
+ );
90
+ act(() => {
91
+ store.setFieldsValue({ options: [{ label: 'a' }, { label: 'b' }] });
92
+ });
93
+ const afterInit = renders;
94
+
95
+ act(() => {
96
+ store.setFieldsValue({ 'options.0.label': 'z' });
97
+ });
98
+ expect(renders).toBe(afterInit);
99
+ });
100
+
101
+ it('订阅按 path 隔离:改无关字段不触发渲染,改本 List path 触发', () => {
102
+ let renders = 0;
103
+ const store = new FormStore().getForm();
104
+ render(
105
+ <FieldContext.Provider value={store}>
106
+ <List name="options">
107
+ {() => {
108
+ renders += 1;
109
+ return null;
110
+ }}
111
+ </List>
112
+ </FieldContext.Provider>,
113
+ );
114
+ const before = renders;
115
+
116
+ act(() => {
117
+ store.setFieldsValue({ other: 'x' });
118
+ });
119
+ expect(renders).toBe(before);
120
+
121
+ act(() => {
122
+ store.setFieldsValue({ options: [{ label: 'a' }] });
123
+ });
124
+ expect(renders).toBeGreaterThan(before);
125
+ });
126
+ });
package/src/Form/List.tsx CHANGED
@@ -28,7 +28,7 @@ const List: React.FC<FormListProps> = ({
28
28
  children,
29
29
  initialValue = [],
30
30
  }) => {
31
- const { getFieldValue, setFieldsValue, validateFields } =
31
+ const { getFieldValue, setFieldsValue, validateFields, subscribe } =
32
32
  useContext(FieldContext);
33
33
 
34
34
  // Normalize list name path
@@ -49,6 +49,9 @@ const List: React.FC<FormListProps> = ({
49
49
  // State for triggering re-renders
50
50
  const [, forceUpdate] = useState(0);
51
51
 
52
+ // 上一次渲染的行数:订阅回调里跳过行内编辑(长度未变)触发的无效重渲染
53
+ const listLengthRef = useRef(0);
54
+
52
55
  // Initialize list value - Fix: use prefixName instead of listNamePath
53
56
  useLayoutEffect(() => {
54
57
  const pathStr = prefixName.join('.');
@@ -59,6 +62,24 @@ const List: React.FC<FormListProps> = ({
59
62
  }
60
63
  }, [prefixName, initialValue, getFieldValue, setFieldsValue]);
61
64
 
65
+ // Subscribe to the list's own path: List re-reads the store only on its own render,
66
+ // so a whole-value replacement via form.setFieldsValue (row count changed) never
67
+ // re-rendered it and new rows never mounted. Field instances already refresh through
68
+ // registerFieldEntities, but the row count lives here. Aligned with rc-field-form.
69
+ useLayoutEffect(
70
+ () =>
71
+ subscribe?.(prefixName.join('.'), () => {
72
+ // isRelatedNamePath 双向前缀匹配,行内编辑(options.i.label)也命中本 watcher;
73
+ // 但 List 渲染输出只取决于行数,行内 Field 会经 registerFieldEntities 自行刷新,
74
+ // 长度未变则跳过,避免大列表逐键全列表重渲染
75
+ const next = getFieldValue?.(prefixName.join('.'));
76
+ const nextLen = Array.isArray(next) ? next.length : 0;
77
+ if (nextLen === listLengthRef.current) return;
78
+ forceUpdate((prev) => prev + 1);
79
+ }),
80
+ [prefixName, subscribe, getFieldValue],
81
+ );
82
+
62
83
  // Get current list value
63
84
  const getListValue = (): any[] => {
64
85
  const pathStr = prefixName.join('.');
@@ -120,7 +141,6 @@ const List: React.FC<FormListProps> = ({
120
141
  setFieldsValue?.({ [pathStr]: newList }, 'update' as any);
121
142
  }
122
143
  keyManager.id += 1;
123
- forceUpdate((prev) => prev + 1);
124
144
  };
125
145
 
126
146
  const remove = (index: number | number[]) => {
@@ -137,7 +157,6 @@ const List: React.FC<FormListProps> = ({
137
157
  // Update list values
138
158
  const newList = currentValue.filter((_, i) => !indexSet.has(i));
139
159
  setFieldsValue?.({ [pathStr]: newList }, 'update' as any);
140
- forceUpdate((prev) => prev + 1);
141
160
  };
142
161
 
143
162
  const moveFunc = (from: number, to: number) => {
@@ -160,6 +179,7 @@ const List: React.FC<FormListProps> = ({
160
179
  // Move list values
161
180
  const newList = move(currentValue, from, to);
162
181
  setFieldsValue?.({ [pathStr]: newList }, 'update' as any);
182
+ // 同长度重排不会触发上方订阅回调(长度未变被跳过),须显式重渲染
163
183
  forceUpdate((prev) => prev + 1);
164
184
  };
165
185
 
@@ -168,6 +188,7 @@ const List: React.FC<FormListProps> = ({
168
188
 
169
189
  // Build field list - removed useMemo, compute directly
170
190
  const listValue = getListValue();
191
+ listLengthRef.current = listValue.length;
171
192
 
172
193
  // Ensure enough keys
173
194
  while (keyManager.keys.length < listValue.length) {
@@ -144,6 +144,25 @@ describe('FormStore.initialize initialValues', () => {
144
144
  });
145
145
  });
146
146
 
147
+ describe('FormStore.initialize re-binds callbacks', () => {
148
+ it('re-registers callbacks on remount but applies initialValues once only', () => {
149
+ const store = new FormStore();
150
+ const first = vi.fn();
151
+ const second = vi.fn();
152
+
153
+ store.initialize({ initialValues: { a: 1 }, callbacks: { onValuesChange: first } });
154
+ // 模拟 <Form> 卸载后重挂载(form 实例由外层 useForm 持有而存活):
155
+ // 新一次挂载传入新的 useEvent 闭包,store 必须改绑到它
156
+ store.initialize({ initialValues: { a: 999 }, callbacks: { onValuesChange: second } });
157
+
158
+ store.setFieldsValue({ a: 2 });
159
+ expect(first).not.toHaveBeenCalled();
160
+ expect(second).toHaveBeenCalledTimes(1);
161
+ // initialValues 仍只在首次初始化生效
162
+ expect(store.getFieldValue('a')).toBe(2);
163
+ });
164
+ });
165
+
147
166
  describe('FormStore.setFieldsValue with clear trigger', () => {
148
167
  it('merges over the store instead of replacing it', () => {
149
168
  const store = new FormStore();
@@ -70,6 +70,13 @@ export class FormStore {
70
70
  initialValues?: Store;
71
71
  callbacks?: Callbacks;
72
72
  }) => {
73
+ // callbacks 每次挂载重绑:form 实例可由外层 useForm 持有、跨越 <Form> 卸载/重挂载存活,
74
+ // 若只在此处注册一次,重挂载后 store 仍持旧挂载的 useEvent 闭包,其 ref 已随旧实例
75
+ // 死亡(不再更新),onValuesChange 等会以过期闭包执行。
76
+ if (callbacks) {
77
+ this.setCallbacks(callbacks);
78
+ }
79
+
73
80
  if (!this.initialized) {
74
81
  this.initialized = true;
75
82
 
@@ -78,9 +85,6 @@ export class FormStore {
78
85
  this.initialValues = JSON.parse(JSON.stringify(initialValues));
79
86
  }
80
87
 
81
- if (callbacks) {
82
- this.setCallbacks(callbacks);
83
- }
84
88
  // Only refresh field entities, do not notify watchers (no value changes to watch during initialization)
85
89
  this.fieldEntities.forEach((entity) => {
86
90
  entity?.onStoreChange();
package/src/version.ts CHANGED
@@ -4,7 +4,7 @@ declare global {
4
4
  }
5
5
  }
6
6
 
7
- export const version = "0.14.4";
7
+ export const version = "0.14.6";
8
8
 
9
9
  if (typeof window !== 'undefined') {
10
10
  window.__yee_c_version__ = version;