Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions packages/core/src/__tests__/array.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
})
40 changes: 40 additions & 0 deletions packages/core/src/models/ArrayField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Decorator, Component, any, any[]> {
displayName = 'ArrayField'
indexKeys: Array<string> = []

constructor(
address: FormPathPattern,
Expand All @@ -22,6 +33,7 @@ export class ArrayField<
designable: boolean
) {
super(address, props, form, designable)
this.indexKeys = []
this.makeAutoCleanable()
}

Expand All @@ -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)
})
}
Expand All @@ -59,6 +88,7 @@ export class ArrayField<
deleteCount: 1,
})
this.value.pop()
this.indexKeys.pop()
return this.onInput(this.value)
})
}
Expand All @@ -67,6 +97,7 @@ export class ArrayField<
return action(() => {
if (!isArr(this.value)) {
this.value = []
this.indexKeys = []
}
if (items.length === 0) {
return
Expand All @@ -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)
})
}
Expand All @@ -88,14 +120,20 @@ export class ArrayField<
deleteCount: 1,
})
this.value.splice(index, 1)
this.indexKeys.splice(index, 1)
return this.onInput(this.value)
})
}

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)
})
}
Expand All @@ -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)
})
}
Expand All @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/Field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/models/VoidField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
89 changes: 72 additions & 17 deletions packages/core/src/shared/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,9 +152,19 @@ export const patchFieldStates = (
target: Record<string, GeneralField>,
patches: INodePatch<GeneralField>[]
) => {
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
Expand All @@ -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<string, GeneralField>,
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]
}
Expand Down Expand Up @@ -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)
Expand All @@ -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,
})
}
}
}
})
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export interface INodePatch<T> {
address: string
oldAddress?: string
payload?: T
oldPayload?: Partial<T>
}

export interface IHeartProps<Context> {
Expand Down
7 changes: 5 additions & 2 deletions packages/react/docs/api/components/ArrayField.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ const ArrayComponent = observer(() => {
<>
<div>
{field.value?.map((item, index) => (
<div key={index} style={{ display: 'flex-block', marginBottom: 10 }}>
<div
key={field.getIndexKey(index)}
style={{ display: 'flex-block', marginBottom: 10 }}
>
<Space>
<Field name={index} component={[Input]} />
<Button
Expand Down Expand Up @@ -105,7 +108,7 @@ export default () => (
<div>
{field.value?.map((item, index) => (
<div
key={index}
key={field.getIndexKey(index)}
style={{ display: 'flex-block', marginBottom: 10 }}
>
<Space>
Expand Down
7 changes: 5 additions & 2 deletions packages/react/docs/api/components/ArrayField.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ const ArrayComponent = observer(() => {
<>
<div>
{field.value?.map((item, index) => (
<div key={index} style={{ display: 'flex-block', marginBottom: 10 }}>
<div
key={field.getIndexKey(index)}
style={{ display: 'flex-block', marginBottom: 10 }}
>
<Space>
<Field name={index} component={[Input]} />
<Button
Expand Down Expand Up @@ -105,7 +108,7 @@ export default () => (
<div>
{field.value?.map((item, index) => (
<div
key={index}
key={field.getIndexKey(index)}
style={{ display: 'flex-block', marginBottom: 10 }}
>
<Space>
Expand Down
2 changes: 1 addition & 1 deletion packages/react/docs/api/components/RecursionField.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const ArrayItems = observer((props) => {
<div>
{props.value?.map((item, index) => {
return (
<div key={index} style={{ marginBottom: 10 }}>
<div key={field.getIndexKey(index)} style={{ marginBottom: 10 }}>
<Space>
<RecursionField schema={schema.items} name={index} />
<Button
Expand Down
2 changes: 1 addition & 1 deletion packages/react/docs/api/components/RecursionField.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ const ArrayItems = observer((props) => {
<div>
{props.value?.map((item, index) => {
return (
<div key={index} style={{ marginBottom: 10 }}>
<div key={field.getIndexKey(index)} style={{ marginBottom: 10 }}>
<Space>
<RecursionField schema={schema.items} name={index} />
<Button
Expand Down
Loading
Loading