diff --git a/packages/core/src/__tests__/array.spec.ts b/packages/core/src/__tests__/array.spec.ts index 158284e496d..c955f8cfbe2 100644 --- a/packages/core/src/__tests__/array.spec.ts +++ b/packages/core/src/__tests__/array.spec.ts @@ -1122,3 +1122,43 @@ test('record: find form fields', () => { expect(array.record).toEqual({ array: [{ a: 1 }, { a: 2 }] }) }) + +test('array field splice array state should not destory unexpected field', () => { + const form = attach( + createForm({ + initialValues: { + array: [{ a: 1 }, { a: 2 }, { a: 3 }], + }, + }) + ) + + const array = attach( + form.createArrayField({ + name: 'array', + }) + ) + attach( + form.createField({ + name: '0', + basePath: 'array', + }) + ) + attach( + form.createField({ + name: '1', + basePath: 'array', + }) + ) + attach( + form.createField({ + name: '2', + basePath: 'array', + }) + ) + + array.remove(0) + + array.remove(0) + + expect(Object.keys(form.fields)).toEqual(['array', 'array.0']) +}) diff --git a/packages/core/src/models/ArrayField.ts b/packages/core/src/models/ArrayField.ts index 6e38d1a9273..6891909401c 100644 --- a/packages/core/src/models/ArrayField.ts +++ b/packages/core/src/models/ArrayField.ts @@ -9,11 +9,22 @@ import { Field } from './Field' import { Form } from './Form' import { JSXComponent, IFieldProps, FormPathPattern } from '../types' +const uniqueIdRef = { current: 0 } + +const getUniqueId = () => { + return uniqueIdRef.current++ +} + +const createIndexKey = () => { + return `_$id_${getUniqueId()}_` +} + export class ArrayField< Decorator extends JSXComponent = any, Component extends JSXComponent = any > extends Field { displayName = 'ArrayField' + indexKeys: Array = [] constructor( address: FormPathPattern, @@ -22,6 +33,7 @@ export class ArrayField< designable: boolean ) { super(address, props, form, designable) + this.indexKeys = [] this.makeAutoCleanable() } @@ -32,20 +44,37 @@ export class ArrayField< (newLength, oldLength) => { if (oldLength && !newLength) { cleanupArrayChildren(this, 0) + this.indexKeys = [] } else if (newLength < oldLength) { cleanupArrayChildren(this, newLength) + this.indexKeys = this.indexKeys.slice(0, newLength) } } ) ) } + getIndexKey(index: number) { + if (!this.indexKeys[index]) { + const newKey = createIndexKey() + this.indexKeys[index] = newKey + return newKey + } + return this.indexKeys[index] + } + + getCurrentKeyIndex(key: string) { + return this.indexKeys.indexOf(key) + } + push = (...items: any[]) => { return action(() => { if (!isArr(this.value)) { this.value = [] + this.indexKeys = [] } this.value.push(...items) + this.indexKeys.push(...items.map(createIndexKey)) return this.onInput(this.value) }) } @@ -59,6 +88,7 @@ export class ArrayField< deleteCount: 1, }) this.value.pop() + this.indexKeys.pop() return this.onInput(this.value) }) } @@ -67,6 +97,7 @@ export class ArrayField< return action(() => { if (!isArr(this.value)) { this.value = [] + this.indexKeys = [] } if (items.length === 0) { return @@ -76,6 +107,7 @@ export class ArrayField< insertCount: items.length, }) this.value.splice(index, 0, ...items) + this.indexKeys.splice(index, 0, ...items.map(createIndexKey)) return this.onInput(this.value) }) } @@ -88,6 +120,7 @@ export class ArrayField< deleteCount: 1, }) this.value.splice(index, 1) + this.indexKeys.splice(index, 1) return this.onInput(this.value) }) } @@ -95,7 +128,12 @@ export class ArrayField< shift = () => { if (!isArr(this.value)) return return action(() => { + spliceArrayState(this, { + startIndex: 0, + deleteCount: 1, + }) this.value.shift() + this.indexKeys.shift() return this.onInput(this.value) }) } @@ -110,6 +148,7 @@ export class ArrayField< insertCount: items.length, }) this.value.unshift(...items) + this.indexKeys.unshift(...items.map(createIndexKey)) return this.onInput(this.value) }) } @@ -119,6 +158,7 @@ export class ArrayField< if (fromIndex === toIndex) return return action(() => { move(this.value, fromIndex, toIndex) + move(this.indexKeys, fromIndex, toIndex) exchangeArrayState(this, { fromIndex, toIndex, diff --git a/packages/core/src/models/Field.ts b/packages/core/src/models/Field.ts index 38812ca1f0b..1fa8c16558c 100644 --- a/packages/core/src/models/Field.ts +++ b/packages/core/src/models/Field.ts @@ -160,6 +160,7 @@ export class Field< componentProps: observable, validator: observable.shallow, data: observable.shallow, + parent: observable.computed, component: observable.computed, decorator: observable.computed, errors: observable.computed, diff --git a/packages/core/src/models/VoidField.ts b/packages/core/src/models/VoidField.ts index 5bc7dc5f83e..f8cad557107 100644 --- a/packages/core/src/models/VoidField.ts +++ b/packages/core/src/models/VoidField.ts @@ -80,6 +80,7 @@ export class VoidField< data: observable.shallow, decoratorProps: observable, componentProps: observable, + parent: observable.computed, display: observable.computed, pattern: observable.computed, hidden: observable.computed, diff --git a/packages/core/src/shared/internals.ts b/packages/core/src/shared/internals.ts index e9b899f85ce..2d393c9f2cb 100644 --- a/packages/core/src/shared/internals.ts +++ b/packages/core/src/shared/internals.ts @@ -152,9 +152,19 @@ export const patchFieldStates = ( target: Record, patches: INodePatch[] ) => { - patches.forEach(({ type, address, oldAddress, payload }) => { + patches.forEach(({ type, address, oldAddress, payload, oldPayload }) => { if (type === 'remove') { - destroy(target, address, false) + if (payload) { + // When a payload is passed, the node should be deleted. However, the address may still be used. + // To avoid affecting the address order, set address to undefined. + destroyField(payload, false) + if (target[address] === payload) { + target[address] = undefined + } + } else { + // If only the address is passed without the payload, it means that the address is no longer used, so remove the address directly + delete target[address] + } } else if (type === 'update') { if (payload) { target[address] = payload @@ -163,24 +173,35 @@ export const patchFieldStates = ( } } if (address && payload) { - locateNode(payload, address) + if (oldPayload) { + payload.address = oldPayload.address + payload.path = oldPayload.path + } else { + locateNode(payload, address) + } } } }) } +export const destroyField = (field: GeneralField, forceClear = true) => { + field.dispose() + if (isDataField(field) && forceClear) { + const form = field.form + const path = field.path + form.deleteValuesIn(path) + form.deleteInitialValuesIn(path) + } +} + export const destroy = ( target: Record, address: string, forceClear = true ) => { const field = target[address] - field?.dispose() - if (isDataField(field) && forceClear) { - const form = field.form - const path = field.path - form.deleteValuesIn(path) - form.deleteInitialValuesIn(path) + if (field) { + destroyField(field, forceClear) } delete target[address] } @@ -383,19 +404,27 @@ export const spliceArrayState = ( return index >= startIndex && index < startIndex + insertCount } const isDeleteNode = (identifier: string) => { + const afterStr = identifier.substring(addrLength) + const number = afterStr.match(NumberIndexReg)?.[1] + if (number === undefined) return false + const index = Number(number) + return index >= startIndex && index < startIndex + deleteCount + } + + const isNeedCleanupNode = (identifier: string) => { const preStr = identifier.substring(0, addrLength) const afterStr = identifier.substring(addrLength) const number = afterStr.match(NumberIndexReg)?.[1] if (number === undefined) return false const index = Number(number) return ( - (index > startIndex && - !fields[ - `${preStr}${afterStr.replace(/^\.\d+/, `.${index + deleteCount}`)}` - ]) || - index === startIndex + index >= startIndex && + !fields[ + `${preStr}${afterStr.replace(/^\.\d+/, `.${index + deleteCount}`)}` + ] ) } + const moveIndex = (identifier: string) => { if (offset === 0) return identifier const preStr = identifier.substring(0, addrLength) @@ -416,10 +445,36 @@ export const spliceArrayState = ( address: newIdentifier, oldAddress: identifier, payload: field, + oldPayload: fields[newIdentifier] + ? { + address: fields[newIdentifier].address, + path: fields[newIdentifier].path, + } + : undefined, }) - } - if (isInsertNode(identifier) || isDeleteNode(identifier)) { - fieldPatches.push({ type: 'remove', address: identifier }) + if (isNeedCleanupNode(identifier)) { + fieldPatches.push({ + type: 'remove', + address: identifier, + }) + } + } else if (isInsertNode(identifier)) { + fieldPatches.push({ + type: 'remove', + address: identifier, + }) + } else if (isDeleteNode(identifier)) { + fieldPatches.push({ + type: 'remove', + address: identifier, + payload: field, + }) + if (isNeedCleanupNode(identifier)) { + fieldPatches.push({ + type: 'remove', + address: identifier, + }) + } } } }) diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index e0716a8cb6a..93146ec8f41 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -111,6 +111,7 @@ export interface INodePatch { address: string oldAddress?: string payload?: T + oldPayload?: Partial } export interface IHeartProps { diff --git a/packages/react/docs/api/components/ArrayField.md b/packages/react/docs/api/components/ArrayField.md index 266f5bec9c3..735442c1110 100644 --- a/packages/react/docs/api/components/ArrayField.md +++ b/packages/react/docs/api/components/ArrayField.md @@ -40,7 +40,10 @@ const ArrayComponent = observer(() => { <>
{field.value?.map((item, index) => ( -
+