Skip to content

Commit 83b5f4b

Browse files
authored
fix(core): fix array value changed but not auto clean node (#1742)
1 parent b51a219 commit 83b5f4b

File tree

11 files changed

+294
-35
lines changed

11 files changed

+294
-35
lines changed

packages/core/src/__tests__/field.spec.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1223,3 +1223,177 @@ test('deep nested fields hidden and validate with middle hidden', async () => {
12231223
await form.validate()
12241224
expect(form.invalid).toBeFalsy()
12251225
})
1226+
1227+
test('auto clean with ArrayField', () => {
1228+
const form = attach(createForm())
1229+
attach(
1230+
form.createArrayField({
1231+
name: 'array',
1232+
initialValue: [{}, {}],
1233+
})
1234+
)
1235+
attach(
1236+
form.createField({
1237+
name: '0.aa',
1238+
basePath: 'array',
1239+
})
1240+
)
1241+
attach(
1242+
form.createField({
1243+
name: '1.aa',
1244+
basePath: 'array',
1245+
})
1246+
)
1247+
const array1 = attach(
1248+
form.createArrayField({
1249+
name: 'array1',
1250+
initialValue: [{}, {}],
1251+
})
1252+
)
1253+
attach(
1254+
form.createField({
1255+
name: '0.aa',
1256+
basePath: 'array1',
1257+
})
1258+
)
1259+
attach(
1260+
form.createField({
1261+
name: '1.aa',
1262+
basePath: 'array1',
1263+
})
1264+
)
1265+
const array2 = attach(
1266+
form.createArrayField({
1267+
name: 'array2',
1268+
initialValue: [{}, {}],
1269+
})
1270+
)
1271+
attach(
1272+
form.createField({
1273+
name: '0.aa',
1274+
basePath: 'array2',
1275+
})
1276+
)
1277+
attach(
1278+
form.createField({
1279+
name: '1.aa',
1280+
basePath: 'array2',
1281+
})
1282+
)
1283+
expect(form.fields['array.1.aa']).not.toBeUndefined()
1284+
expect(form.values.array).toEqual([{}, {}])
1285+
form.setValues(
1286+
{
1287+
array: [{}],
1288+
},
1289+
'shallowMerge'
1290+
)
1291+
expect(form.values.array).toEqual([{}])
1292+
expect(form.fields['array.1.aa']).toBeUndefined()
1293+
expect(form.fields['array1.0.aa']).not.toBeUndefined()
1294+
expect(form.fields['array1.1.aa']).not.toBeUndefined()
1295+
expect(form.values.array1).toEqual([{}, {}])
1296+
array1.setValue([])
1297+
expect(form.fields['array1.0.aa']).toBeUndefined()
1298+
expect(form.fields['array1.1.aa']).toBeUndefined()
1299+
expect(form.fields['array2.0.aa']).not.toBeUndefined()
1300+
expect(form.fields['array2.1.aa']).not.toBeUndefined()
1301+
array2.setValue([])
1302+
expect(form.fields['array2.0.aa']).toBeUndefined()
1303+
expect(form.fields['array2.1.aa']).toBeUndefined()
1304+
})
1305+
1306+
test('auto clean with ObjectField', () => {
1307+
const form = attach(createForm())
1308+
attach(
1309+
form.createObjectField({
1310+
name: 'obj',
1311+
initialValue: {
1312+
aa: 'aa',
1313+
bb: 'bb',
1314+
},
1315+
})
1316+
)
1317+
attach(
1318+
form.createField({
1319+
name: 'aa',
1320+
basePath: 'obj',
1321+
})
1322+
)
1323+
attach(
1324+
form.createField({
1325+
name: 'bb',
1326+
basePath: 'obj',
1327+
})
1328+
)
1329+
const obj1 = attach(
1330+
form.createObjectField({
1331+
name: 'obj1',
1332+
initialValue: {
1333+
aa: 'aa',
1334+
bb: 'bb',
1335+
},
1336+
})
1337+
)
1338+
attach(
1339+
form.createField({
1340+
name: 'aa',
1341+
basePath: 'obj1',
1342+
})
1343+
)
1344+
attach(
1345+
form.createField({
1346+
name: 'bb',
1347+
basePath: 'obj1',
1348+
})
1349+
)
1350+
const obj2 = attach(
1351+
form.createObjectField({
1352+
name: 'obj2',
1353+
initialValue: {
1354+
aa: 'aa',
1355+
bb: 'bb',
1356+
},
1357+
})
1358+
)
1359+
attach(
1360+
form.createField({
1361+
name: 'aa',
1362+
basePath: 'obj2',
1363+
})
1364+
)
1365+
attach(
1366+
form.createField({
1367+
name: 'bb',
1368+
basePath: 'obj2',
1369+
})
1370+
)
1371+
expect(form.fields['obj.aa']).not.toBeUndefined()
1372+
expect(form.fields['obj.bb']).not.toBeUndefined()
1373+
expect(form.values.obj).toEqual({ aa: 'aa', bb: 'bb' })
1374+
form.setValues(
1375+
{
1376+
obj: {
1377+
aa: '123',
1378+
},
1379+
},
1380+
'shallowMerge'
1381+
)
1382+
expect(form.values.obj).toEqual({ aa: '123' })
1383+
expect(form.fields['obj.aa']).not.toBeUndefined()
1384+
expect(form.fields['obj.bb']).toBeUndefined()
1385+
expect(form.fields['obj1.aa']).not.toBeUndefined()
1386+
expect(form.fields['obj1.bb']).not.toBeUndefined()
1387+
expect(form.values.obj1).toEqual({ aa: 'aa', bb: 'bb' })
1388+
obj1.setValue({})
1389+
expect(form.values.obj1).toEqual({})
1390+
expect(form.fields['obj1.aa']).toBeUndefined()
1391+
expect(form.fields['obj1.bb']).toBeUndefined()
1392+
expect(form.fields['obj2.aa']).not.toBeUndefined()
1393+
expect(form.fields['obj2.bb']).not.toBeUndefined()
1394+
expect(form.values.obj2).toEqual({ aa: 'aa', bb: 'bb' })
1395+
obj2.setValue({ aa: 'aa', bb: 'bb', cc: 'cc' })
1396+
expect(form.fields['obj2.aa']).not.toBeUndefined()
1397+
expect(form.fields['obj2.bb']).not.toBeUndefined()
1398+
expect(form.fields['obj2.cc']).toBeUndefined()
1399+
})

packages/core/src/models/ArrayField.ts

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { isArr } from '@formily/shared'
2-
import { batch } from '@formily/reactive'
3-
import { spliceArrayState, exchangeArrayState } from '../shared/internals'
2+
import { batch, reaction } from '@formily/reactive'
3+
import {
4+
spliceArrayState,
5+
exchangeArrayState,
6+
cleanupArrayChildren,
7+
} from '../shared/internals'
48
import { Field } from './Field'
59
import { Form } from './Form'
610
import { JSXComponent, IFieldProps, FormPathPattern } from '../types'
@@ -17,14 +21,22 @@ export class ArrayField<
1721
form: Form,
1822
designable: boolean
1923
) {
20-
super(
21-
address,
22-
{
23-
...props,
24-
value: isArr(props.value) ? props.value : [],
25-
},
26-
form,
27-
designable
24+
super(address, props, form, designable)
25+
this.addAutoCleaner()
26+
}
27+
28+
protected addAutoCleaner() {
29+
this.disposers.push(
30+
reaction(
31+
() => this.value?.length,
32+
(newLength, oldLength) => {
33+
if (oldLength && !newLength) {
34+
cleanupArrayChildren(this, 0)
35+
} else if (newLength < oldLength) {
36+
cleanupArrayChildren(this, newLength)
37+
}
38+
}
39+
)
2840
)
2941
}
3042

packages/core/src/models/Field.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ export class Field<
9494
form: Form
9595
props: IFieldProps<Decorator, Component, TextType, ValueType>
9696

97-
private caches: IFieldCaches = {}
98-
private requests: IFieldRequests = {}
99-
private disposers: (() => void)[] = []
97+
protected caches: IFieldCaches = {}
98+
protected requests: IFieldRequests = {}
99+
protected disposers: (() => void)[] = []
100100

101101
constructor(
102102
address: FormPathPattern,

packages/core/src/models/Form.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
defaults,
1818
clone,
1919
isPlainObj,
20+
isArr,
21+
isObj,
2022
} from '@formily/shared'
2123
import { Heart } from './Heart'
2224
import { Field } from './Field'
@@ -329,7 +331,10 @@ export class Form<ValueType extends object = any> {
329331
batch(() => {
330332
this.fields[identifier] = new ArrayField(
331333
address,
332-
props,
334+
{
335+
...props,
336+
value: isArr(props.value) ? props.value : [],
337+
},
333338
this,
334339
this.props.designable
335340
)
@@ -352,7 +357,10 @@ export class Form<ValueType extends object = any> {
352357
batch(() => {
353358
this.fields[identifier] = new ObjectField(
354359
address,
355-
props,
360+
{
361+
...props,
362+
value: isObj(props.value) ? props.value : {},
363+
},
356364
this,
357365
this.props.designable
358366
)

packages/core/src/models/ObjectField.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { FormPathPattern, isObj } from '@formily/shared'
1+
import { FormPathPattern } from '@formily/shared'
2+
import { reaction } from '@formily/reactive'
23
import { JSXComponent, IFieldProps } from '../types'
34
import { Field } from './Field'
45
import { Form } from './Form'
6+
import { cleanupObjectChildren } from '../shared/internals'
57
export class ObjectField<
68
Decorator extends JSXComponent = any,
79
Component extends JSXComponent = any
@@ -14,14 +16,21 @@ export class ObjectField<
1416
form: Form,
1517
designable: boolean
1618
) {
17-
super(
18-
address,
19-
{
20-
...props,
21-
value: isObj(props.value) ? props.value : {},
22-
},
23-
form,
24-
designable
19+
super(address, props, form, designable)
20+
this.addAutoCleaner()
21+
}
22+
23+
protected addAutoCleaner() {
24+
this.disposers.push(
25+
reaction(
26+
() => Object.keys(this.value || {}),
27+
(newKeys, oldKeys) => {
28+
cleanupObjectChildren(
29+
this,
30+
oldKeys.filter((key) => !newKeys.includes(key))
31+
)
32+
}
33+
)
2534
)
2635
}
2736

packages/core/src/models/VoidField.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export class VoidField<Decorator = any, Component = any, TextType = any> {
5050
form: Form
5151
props: IVoidFieldProps<Decorator, Component>
5252

53-
private disposers: (() => void)[] = []
53+
protected disposers: (() => void)[] = []
5454

5555
constructor(
5656
address: FormPathPattern,

packages/core/src/shared/internals.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import {
1313
} from '@formily/shared'
1414
import { ValidatorTriggerType, validate } from '@formily/validator'
1515
import { action, batch, toJS } from '@formily/reactive'
16-
import { Field, ArrayField, Form } from '../models'
16+
import { Field, ArrayField, Form, ObjectField } from '../models'
1717
import {
1818
ISpliceArrayStateProps,
1919
IExchangeArrayStateProps,
@@ -105,6 +105,7 @@ export const applyFieldPatches = (
105105
) => {
106106
patches.forEach(({ type, address, payload }) => {
107107
if (type === 'remove') {
108+
target[address].dispose()
108109
delete target[address]
109110
} else if (type === 'update') {
110111
if (payload) {
@@ -355,6 +356,61 @@ export const exchangeArrayState = (
355356
field.form.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE)
356357
}
357358

359+
export const cleanupArrayChildren = (field: ArrayField, start: number) => {
360+
const address = field.address.toString()
361+
const fields = field.form.fields
362+
363+
const isArrayChildren = (identifier: string) => {
364+
return (
365+
identifier.indexOf(address) === 0 && identifier.length > address.length
366+
)
367+
}
368+
369+
const isNeedCleanup = (identifier: string) => {
370+
const afterStr = identifier.slice(address.length)
371+
const number = afterStr.match(/^\.(\d+)/)?.[1]
372+
if (number === undefined) return false
373+
const index = Number(number)
374+
return index >= start
375+
}
376+
377+
batch(() => {
378+
each(fields, (field, identifier) => {
379+
if (isArrayChildren(identifier) && isNeedCleanup(identifier)) {
380+
field.dispose()
381+
delete fields[identifier]
382+
}
383+
})
384+
})
385+
}
386+
387+
export const cleanupObjectChildren = (field: ObjectField, keys: string[]) => {
388+
const address = field.address.toString()
389+
const fields = field.form.fields
390+
391+
const isObjectChildren = (identifier: string) => {
392+
return (
393+
identifier.indexOf(address) === 0 && identifier.length > address.length
394+
)
395+
}
396+
397+
const isNeedCleanup = (identifier: string) => {
398+
const afterStr = identifier.slice(address.length)
399+
const key = afterStr.match(/^\.([^.]+)/)?.[1]
400+
if (key === undefined) return false
401+
return keys.includes(key)
402+
}
403+
404+
batch(() => {
405+
each(fields, (field, identifier) => {
406+
if (isObjectChildren(identifier) && isNeedCleanup(identifier)) {
407+
field.dispose()
408+
delete fields[identifier]
409+
}
410+
})
411+
})
412+
}
413+
358414
export const isEmptyWithField = (field: GeneralField, value: any) => {
359415
if (isArrayField(field) || isObjectField(field)) {
360416
return isEmpty(value, true)

0 commit comments

Comments
 (0)