Skip to content

Commit eb7f355

Browse files
committed
feat: optimize the rendering and operation methods of the list component to support useFieldArray. Add an ArrayKeys type utility function to directly find the keys of the array of the given type.
1 parent 21d6fe9 commit eb7f355

File tree

4 files changed

+116
-60
lines changed

4 files changed

+116
-60
lines changed

primitives/filed-form /src/form-core/createStore.ts

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable no-plusplus */
12
/* eslint-disable no-nested-ternary */
23
/* eslint-disable complexity */
34
/* eslint-disable @typescript-eslint/no-unused-expressions */
@@ -22,15 +23,17 @@ import type { Rule, ValidateOptions } from './validation';
2223

2324
/**
2425
* Listener type definition for form state changes
25-
* @type {Object}
26-
* @property {Function} cb - Callback function triggered on state changes
27-
* @property {ChangeMask} mask - Bit mask indicating which types of changes to listen for
2826
*/
2927
type Listener = {
3028
cb: (value: StoreValue, key: string, all: Store, fired: ChangeMask) => void;
3129
mask: ChangeMask;
3230
};
3331

32+
export type ArrayField = {
33+
id: number;
34+
keys: number[];
35+
};
36+
3437
const matchTrigger = (rule: Rule, trig?: string | string[]) => {
3538
const list = toArray(rule.validateTrigger);
3639

@@ -55,6 +58,13 @@ function getFlag(bucket: Set<string>, names?: NamePath[]) {
5558
return anyOn(bucket, names);
5659
}
5760

61+
function move<T>(arr: T[], from: number, to: number): T[] {
62+
const clone = arr.slice();
63+
const item = clone.splice(from, 1)[0];
64+
clone.splice(to, 0, item);
65+
return clone;
66+
}
67+
5868
/**
5969
* FormStore class - Core form state management implementation
6070
* Handles form state, validation, field registration, and state subscriptions
@@ -70,6 +80,8 @@ class FormStore {
7080
/** Registry of field entities */
7181
private _fieldEntities: FieldEntity[] = [];
7282

83+
private arrayKeyMap = new Map<string, ArrayField>();
84+
7385
/** Form lifecycle callbacks */
7486
private _callbacks: Callbacks = {};
7587
/** Validation message templates */
@@ -156,32 +168,27 @@ class FormStore {
156168
// ------------------------------------------------
157169
/**
158170
* Determines if field values should be preserved based on field and global settings
159-
* @param fieldPreserve - Optional field-level preserve setting
160-
* @returns {boolean} Final preserve setting
161171
*/
162172
private isMergedPreserve = (fieldPreserve?: boolean) => {
163173
return fieldPreserve ?? this._preserve;
164174
};
165175

166176
/**
167177
* Sets form lifecycle callbacks
168-
* @param c - Callback functions object
169178
*/
170179
private setCallbacks = (c: Callbacks) => {
171180
this._callbacks = c || {};
172181
};
173182

174183
/**
175184
* Sets validation message templates
176-
* @param m - Validation message templates object
177185
*/
178186
private setValidateMessages = (m: ValidateMessages) => {
179187
this._validateMessages = m || {};
180188
};
181189

182190
/**
183191
* Sets global preserve flag for field values
184-
* @param preserve - Whether to preserve field values after unmount
185192
*/
186193
private setPreserve = (preserve: boolean) => {
187194
this._preserve = preserve;
@@ -192,7 +199,6 @@ class FormStore {
192199
// ------------------------------------------------
193200
/**
194201
* Adds a new middleware to the middleware stack
195-
* @param mw - Middleware function to add
196202
*/
197203
private use = (mw: Middleware) => {
198204
this._middlewares.push(mw);
@@ -214,7 +220,6 @@ class FormStore {
214220
// Action Dispatch System
215221
/**
216222
* Base dispatch function that handles all form actions
217-
* @param a - Action object containing type and payload
218223
*/
219224
private baseDispatch = (a: Action) => {
220225
switch (a.type) {
@@ -260,7 +265,6 @@ class FormStore {
260265

261266
/**
262267
* Enhanced dispatch function that processes actions through middleware chain
263-
* @param a - Action to be dispatched
264268
*/
265269
private dispatch = (a: Action) => {
266270
const ctx = { dispatch: (x: Action) => this.dispatch(x), getState: () => this._store };
@@ -274,15 +278,13 @@ class FormStore {
274278
// ------------------------------------------------
275279
/**
276280
* Updates the main form state store
277-
* @param nextStore - New store state to set
278281
*/
279282
private updateStore = (nextStore: Store) => {
280283
this._store = nextStore;
281284
};
282285

283286
/**
284287
* Sets initial form values and updates current store
285-
* @param values - Initial values to set
286288
*/
287289
private setInitialValues = (values: Store) => {
288290
this._initial = values;
@@ -294,7 +296,6 @@ class FormStore {
294296

295297
/**
296298
* Gets current form state including validation status
297-
* @returns {Object} Current form state object
298299
*/
299300
private getFormState() {
300301
return {
@@ -310,8 +311,6 @@ class FormStore {
310311

311312
/**
312313
* Gets initial value for a field
313-
* @param name - Field name path
314-
* @returns Initial value for the field
315314
*/
316315
private getInitialValue = (name: NamePath) => {
317316
return get(this._initial, keyOfName(name));
@@ -410,6 +409,7 @@ class FormStore {
410409
this._fieldEntities = this._fieldEntities.filter(e => e.name !== name);
411410
this._initial = unset(this._initial, name);
412411
this._store = unset(this._store, name);
412+
this.arrayKeyMap.delete(name);
413413
}
414414
this._exactListeners.delete(name);
415415
this._prefixListeners.delete(name);
@@ -980,18 +980,34 @@ class FormStore {
980980
}
981981

982982
// ===== Array Operation =====
983+
984+
private getArrayKeyManager = (name: string) => {
985+
let mgr = this.arrayKeyMap.get(name);
986+
if (!mgr) {
987+
mgr = { id: 0, keys: [] };
988+
this.arrayKeyMap.set(name, mgr);
989+
}
990+
return mgr;
991+
};
992+
983993
private arrayOp = (name: NamePath, op: 'insert' | 'move' | 'remove' | 'replace' | 'swap', args: any) => {
984994
const arr = this.getFieldValue(name);
985995
if (!Array.isArray(arr)) return;
986996
const next = arr.slice();
987997
const ak = keyOfName(name);
998+
999+
const keyMgr = this.getArrayKeyManager(ak);
1000+
9881001
const affected3 = this.collectDependents([ak]);
9891002
this.recomputeTargets(affected3);
1003+
9901004
const mark = (mask: ChangeMask = ChangeTag.Value) => this.enqueueNotify([name], mask);
1005+
9911006
switch (op) {
9921007
case 'insert': {
9931008
const { index, item } = args;
9941009
next.splice(index, 0, item);
1010+
keyMgr.keys.splice(index, 0, (keyMgr.id += 1));
9951011
this._store = set(this._store, name, next);
9961012
this._validated.delete(ak);
9971013
mark();
@@ -1000,6 +1016,7 @@ class FormStore {
10001016
case 'remove': {
10011017
const { index } = args;
10021018
next.splice(index, 1);
1019+
keyMgr.keys.splice(index, 1);
10031020
this._store = set(this._store, name, next);
10041021
this._validated.delete(ak);
10051022
mark();
@@ -1009,6 +1026,7 @@ class FormStore {
10091026
const { from, to } = args;
10101027
const [x] = next.splice(from, 1);
10111028
next.splice(to, 0, x);
1029+
keyMgr.keys = move(keyMgr.keys, from, to);
10121030
this._store = set(this._store, name, next);
10131031
this._validated.delete(ak);
10141032
mark();
@@ -1019,6 +1037,7 @@ class FormStore {
10191037
const tmp = next[i];
10201038
next[i] = next[j];
10211039
next[j] = tmp;
1040+
[keyMgr.keys[i], keyMgr.keys[j]] = [keyMgr.keys[j], keyMgr.keys[i]];
10221041
this._store = set(this._store, name, next);
10231042
this._validated.delete(ak);
10241043
mark();
@@ -1037,6 +1056,24 @@ class FormStore {
10371056
}
10381057
};
10391058

1059+
private getArrayFields = (name: NamePath, initialValue?: StoreValue[]) => {
1060+
const arr = (this.getFieldValue(name) as any[]) || initialValue || [];
1061+
1062+
const ak = keyOfName(name);
1063+
1064+
const keyMgr = this.getArrayKeyManager(ak);
1065+
1066+
return arr.map((___, i) => {
1067+
if (keyMgr.keys[i] === undefined) {
1068+
keyMgr.keys[i] = keyMgr.id++;
1069+
}
1070+
return {
1071+
key: String(keyMgr.keys[i]),
1072+
name: `${ak}.${i}`
1073+
};
1074+
});
1075+
};
1076+
10401077
// ===== FieldChange =====
10411078
triggerOnFieldsChange = (nameList: NamePath[]) => {
10421079
if (this._callbacks?.onFieldsChange) {
@@ -1280,6 +1317,7 @@ class FormStore {
12801317
return {
12811318
destroyForm: this.destroyForm,
12821319
dispatch: this.dispatch,
1320+
getArrayFields: this.getArrayFields,
12831321
getInitialValue: this.getInitialValue,
12841322
registerComputed: this.registerComputed,
12851323
registerField: this.registerField,

primitives/filed-form /src/react/components/List.tsx

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,79 +3,66 @@
33
/* eslint-disable react/hook-use-state */
44
/* eslint-disable no-plusplus */
55
import React, { useEffect, useRef, useState } from 'react';
6-
import type { AllPaths } from 'skyroc-type-utils';
6+
import type { ArrayKeys } from 'skyroc-type-utils';
77

88
import type { StoreValue } from '../../form-core/types';
9-
import { keyOfName } from '../../utils/util';
10-
import type { InternalFormInstance } from '../hooks/FieldContext';
9+
import type { InternalFormInstance, ListRenderItem } from '../hooks/FieldContext';
1110
import { useFieldContext } from '../hooks/FieldContext';
1211

13-
export type ListRenderItem = {
14-
key: string;
15-
name: string;
16-
};
17-
12+
/* - ListProps: props for the List component */
1813
export type ListProps<Values = any> = {
19-
// array path
14+
/* - children: render function receiving 'fields' and array operation helpers */
2015
children: (
16+
/* - fields: array of item descriptors to render */
2117
fields: ListRenderItem[],
18+
/* - ops: mutation helpers for the array field */
2219
ops: {
20+
/* - insert: insert item at index */
2321
insert: (index: number, item: any) => void;
22+
/* - move: move item from one index to another */
2423
move: (from: number, to: number) => void;
24+
/* - remove: remove item at index */
2525
remove: (index: number) => void;
26+
/* - replace: replace item at index with a new value */
2627
replace: (index: number, val: any) => void;
28+
/* - swap: swap two items by their indices */
2729
swap: (i: number, j: number) => void;
2830
}
2931
) => React.ReactNode;
32+
33+
/* - initialValue: default array value for this field */
3034
initialValue?: StoreValue[];
31-
name: AllPaths<Values>;
32-
};
3335

34-
function move<T>(arr: T[], from: number, to: number): T[] {
35-
const clone = arr.slice();
36-
const item = clone.splice(from, 1)[0];
37-
clone.splice(to, 0, item);
38-
return clone;
39-
}
36+
/* - name: form path pointing to an array field */
37+
name: ArrayKeys<Values> & string;
38+
39+
/* - preserve: keep field state when unmounted (default: true) */
40+
preserve?: boolean;
41+
};
4042

4143
function List<Values = any>(props: ListProps<Values>) {
42-
const { children, initialValue, name } = props;
44+
const { children, initialValue, name, preserve = true } = props;
4345

4446
const fieldContext = useFieldContext<Values>();
4547

46-
const keyRef = useRef({ id: 0, keys: [] as number[] });
47-
const keyManager = keyRef.current;
48-
49-
const { getFieldValue, getInternalHooks } = fieldContext as unknown as InternalFormInstance<Values>;
48+
const { getInternalHooks } = fieldContext as unknown as InternalFormInstance<Values>;
5049

51-
const { dispatch, registerField } = getInternalHooks();
50+
const { dispatch, getArrayFields, registerField } = getInternalHooks();
5251

5352
const [_, forceUpdate] = useState({});
5453

55-
const arr = (getFieldValue(name) as any[]) || initialValue || [];
56-
57-
const fields = arr.map((___, i) => {
58-
let key = keyManager.keys[i];
59-
if (key === undefined) {
60-
keyManager.keys[i] = keyManager.id++;
61-
key = keyManager.keys[i];
62-
}
63-
return {
64-
key: String(key), // Stable key
65-
name: `${keyOfName(name)}.${i}`
66-
};
67-
});
54+
const fields = getArrayFields(name, initialValue);
6855

6956
const unregisterRef = useRef<() => void>(null);
7057

7158
if (!unregisterRef.current) {
7259
unregisterRef.current = registerField({
73-
changeValue: (newValue, __, ___, mask) => {
60+
changeValue: () => {
7461
forceUpdate({});
7562
},
7663
initialValue,
7764
name,
78-
preserve: true
65+
preserve
7966
});
8067
}
8168

@@ -88,22 +75,18 @@ function List<Values = any>(props: ListProps<Values>) {
8875
const ops = {
8976
insert: (index: number, item: any) => {
9077
dispatch({ args: { index, item }, name, op: 'insert', type: 'arrayOp' });
91-
keyManager.keys.splice(index, 0, keyManager.id++);
9278
},
9379
move: (from: number, to: number) => {
9480
dispatch({ args: { from, to }, name, op: 'move', type: 'arrayOp' });
95-
keyManager.keys = move(keyManager.keys, from, to);
9681
},
9782
remove: (index: number) => {
9883
dispatch({ args: { index }, name, op: 'remove', type: 'arrayOp' });
99-
keyManager.keys.splice(index, 1);
10084
},
10185
replace: (index: number, val: any) => {
10286
dispatch({ args: { index, item: val }, name, op: 'replace', type: 'arrayOp' });
10387
},
10488
swap: (i: number, j: number) => {
10589
dispatch({ args: { i, j }, name, op: 'swap', type: 'arrayOp' });
106-
[keyManager.keys[i], keyManager.keys[j]] = [keyManager.keys[j], keyManager.keys[i]];
10790
}
10891
};
10992

0 commit comments

Comments
 (0)