Skip to content

Commit

Permalink
fix(core): fix array value changed but not auto clean node (#1742)
Browse files Browse the repository at this point in the history
  • Loading branch information
janryWang authored Jul 7, 2021
1 parent b51a219 commit 83b5f4b
Show file tree
Hide file tree
Showing 11 changed files with 294 additions and 35 deletions.
174 changes: 174 additions & 0 deletions packages/core/src/__tests__/field.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1223,3 +1223,177 @@ test('deep nested fields hidden and validate with middle hidden', async () => {
await form.validate()
expect(form.invalid).toBeFalsy()
})

test('auto clean with ArrayField', () => {
const form = attach(createForm())
attach(
form.createArrayField({
name: 'array',
initialValue: [{}, {}],
})
)
attach(
form.createField({
name: '0.aa',
basePath: 'array',
})
)
attach(
form.createField({
name: '1.aa',
basePath: 'array',
})
)
const array1 = attach(
form.createArrayField({
name: 'array1',
initialValue: [{}, {}],
})
)
attach(
form.createField({
name: '0.aa',
basePath: 'array1',
})
)
attach(
form.createField({
name: '1.aa',
basePath: 'array1',
})
)
const array2 = attach(
form.createArrayField({
name: 'array2',
initialValue: [{}, {}],
})
)
attach(
form.createField({
name: '0.aa',
basePath: 'array2',
})
)
attach(
form.createField({
name: '1.aa',
basePath: 'array2',
})
)
expect(form.fields['array.1.aa']).not.toBeUndefined()
expect(form.values.array).toEqual([{}, {}])
form.setValues(
{
array: [{}],
},
'shallowMerge'
)
expect(form.values.array).toEqual([{}])
expect(form.fields['array.1.aa']).toBeUndefined()
expect(form.fields['array1.0.aa']).not.toBeUndefined()
expect(form.fields['array1.1.aa']).not.toBeUndefined()
expect(form.values.array1).toEqual([{}, {}])
array1.setValue([])
expect(form.fields['array1.0.aa']).toBeUndefined()
expect(form.fields['array1.1.aa']).toBeUndefined()
expect(form.fields['array2.0.aa']).not.toBeUndefined()
expect(form.fields['array2.1.aa']).not.toBeUndefined()
array2.setValue([])
expect(form.fields['array2.0.aa']).toBeUndefined()
expect(form.fields['array2.1.aa']).toBeUndefined()
})

test('auto clean with ObjectField', () => {
const form = attach(createForm())
attach(
form.createObjectField({
name: 'obj',
initialValue: {
aa: 'aa',
bb: 'bb',
},
})
)
attach(
form.createField({
name: 'aa',
basePath: 'obj',
})
)
attach(
form.createField({
name: 'bb',
basePath: 'obj',
})
)
const obj1 = attach(
form.createObjectField({
name: 'obj1',
initialValue: {
aa: 'aa',
bb: 'bb',
},
})
)
attach(
form.createField({
name: 'aa',
basePath: 'obj1',
})
)
attach(
form.createField({
name: 'bb',
basePath: 'obj1',
})
)
const obj2 = attach(
form.createObjectField({
name: 'obj2',
initialValue: {
aa: 'aa',
bb: 'bb',
},
})
)
attach(
form.createField({
name: 'aa',
basePath: 'obj2',
})
)
attach(
form.createField({
name: 'bb',
basePath: 'obj2',
})
)
expect(form.fields['obj.aa']).not.toBeUndefined()
expect(form.fields['obj.bb']).not.toBeUndefined()
expect(form.values.obj).toEqual({ aa: 'aa', bb: 'bb' })
form.setValues(
{
obj: {
aa: '123',
},
},
'shallowMerge'
)
expect(form.values.obj).toEqual({ aa: '123' })
expect(form.fields['obj.aa']).not.toBeUndefined()
expect(form.fields['obj.bb']).toBeUndefined()
expect(form.fields['obj1.aa']).not.toBeUndefined()
expect(form.fields['obj1.bb']).not.toBeUndefined()
expect(form.values.obj1).toEqual({ aa: 'aa', bb: 'bb' })
obj1.setValue({})
expect(form.values.obj1).toEqual({})
expect(form.fields['obj1.aa']).toBeUndefined()
expect(form.fields['obj1.bb']).toBeUndefined()
expect(form.fields['obj2.aa']).not.toBeUndefined()
expect(form.fields['obj2.bb']).not.toBeUndefined()
expect(form.values.obj2).toEqual({ aa: 'aa', bb: 'bb' })
obj2.setValue({ aa: 'aa', bb: 'bb', cc: 'cc' })
expect(form.fields['obj2.aa']).not.toBeUndefined()
expect(form.fields['obj2.bb']).not.toBeUndefined()
expect(form.fields['obj2.cc']).toBeUndefined()
})
32 changes: 22 additions & 10 deletions packages/core/src/models/ArrayField.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { isArr } from '@formily/shared'
import { batch } from '@formily/reactive'
import { spliceArrayState, exchangeArrayState } from '../shared/internals'
import { batch, reaction } from '@formily/reactive'
import {
spliceArrayState,
exchangeArrayState,
cleanupArrayChildren,
} from '../shared/internals'
import { Field } from './Field'
import { Form } from './Form'
import { JSXComponent, IFieldProps, FormPathPattern } from '../types'
Expand All @@ -17,14 +21,22 @@ export class ArrayField<
form: Form,
designable: boolean
) {
super(
address,
{
...props,
value: isArr(props.value) ? props.value : [],
},
form,
designable
super(address, props, form, designable)
this.addAutoCleaner()
}

protected addAutoCleaner() {
this.disposers.push(
reaction(
() => this.value?.length,
(newLength, oldLength) => {
if (oldLength && !newLength) {
cleanupArrayChildren(this, 0)
} else if (newLength < oldLength) {
cleanupArrayChildren(this, newLength)
}
}
)
)
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/models/Field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,9 +94,9 @@ export class Field<
form: Form
props: IFieldProps<Decorator, Component, TextType, ValueType>

private caches: IFieldCaches = {}
private requests: IFieldRequests = {}
private disposers: (() => void)[] = []
protected caches: IFieldCaches = {}
protected requests: IFieldRequests = {}
protected disposers: (() => void)[] = []

constructor(
address: FormPathPattern,
Expand Down
12 changes: 10 additions & 2 deletions packages/core/src/models/Form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
defaults,
clone,
isPlainObj,
isArr,
isObj,
} from '@formily/shared'
import { Heart } from './Heart'
import { Field } from './Field'
Expand Down Expand Up @@ -329,7 +331,10 @@ export class Form<ValueType extends object = any> {
batch(() => {
this.fields[identifier] = new ArrayField(
address,
props,
{
...props,
value: isArr(props.value) ? props.value : [],
},
this,
this.props.designable
)
Expand All @@ -352,7 +357,10 @@ export class Form<ValueType extends object = any> {
batch(() => {
this.fields[identifier] = new ObjectField(
address,
props,
{
...props,
value: isObj(props.value) ? props.value : {},
},
this,
this.props.designable
)
Expand Down
27 changes: 18 additions & 9 deletions packages/core/src/models/ObjectField.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { FormPathPattern, isObj } from '@formily/shared'
import { FormPathPattern } from '@formily/shared'
import { reaction } from '@formily/reactive'
import { JSXComponent, IFieldProps } from '../types'
import { Field } from './Field'
import { Form } from './Form'
import { cleanupObjectChildren } from '../shared/internals'
export class ObjectField<
Decorator extends JSXComponent = any,
Component extends JSXComponent = any
Expand All @@ -14,14 +16,21 @@ export class ObjectField<
form: Form,
designable: boolean
) {
super(
address,
{
...props,
value: isObj(props.value) ? props.value : {},
},
form,
designable
super(address, props, form, designable)
this.addAutoCleaner()
}

protected addAutoCleaner() {
this.disposers.push(
reaction(
() => Object.keys(this.value || {}),
(newKeys, oldKeys) => {
cleanupObjectChildren(
this,
oldKeys.filter((key) => !newKeys.includes(key))
)
}
)
)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/models/VoidField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export class VoidField<Decorator = any, Component = any, TextType = any> {
form: Form
props: IVoidFieldProps<Decorator, Component>

private disposers: (() => void)[] = []
protected disposers: (() => void)[] = []

constructor(
address: FormPathPattern,
Expand Down
58 changes: 57 additions & 1 deletion packages/core/src/shared/internals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
} from '@formily/shared'
import { ValidatorTriggerType, validate } from '@formily/validator'
import { action, batch, toJS } from '@formily/reactive'
import { Field, ArrayField, Form } from '../models'
import { Field, ArrayField, Form, ObjectField } from '../models'
import {
ISpliceArrayStateProps,
IExchangeArrayStateProps,
Expand Down Expand Up @@ -105,6 +105,7 @@ export const applyFieldPatches = (
) => {
patches.forEach(({ type, address, payload }) => {
if (type === 'remove') {
target[address].dispose()
delete target[address]
} else if (type === 'update') {
if (payload) {
Expand Down Expand Up @@ -355,6 +356,61 @@ export const exchangeArrayState = (
field.form.notify(LifeCycleTypes.ON_FORM_GRAPH_CHANGE)
}

export const cleanupArrayChildren = (field: ArrayField, start: number) => {
const address = field.address.toString()
const fields = field.form.fields

const isArrayChildren = (identifier: string) => {
return (
identifier.indexOf(address) === 0 && identifier.length > address.length
)
}

const isNeedCleanup = (identifier: string) => {
const afterStr = identifier.slice(address.length)
const number = afterStr.match(/^\.(\d+)/)?.[1]
if (number === undefined) return false
const index = Number(number)
return index >= start
}

batch(() => {
each(fields, (field, identifier) => {
if (isArrayChildren(identifier) && isNeedCleanup(identifier)) {
field.dispose()
delete fields[identifier]
}
})
})
}

export const cleanupObjectChildren = (field: ObjectField, keys: string[]) => {
const address = field.address.toString()
const fields = field.form.fields

const isObjectChildren = (identifier: string) => {
return (
identifier.indexOf(address) === 0 && identifier.length > address.length
)
}

const isNeedCleanup = (identifier: string) => {
const afterStr = identifier.slice(address.length)
const key = afterStr.match(/^\.([^.]+)/)?.[1]
if (key === undefined) return false
return keys.includes(key)
}

batch(() => {
each(fields, (field, identifier) => {
if (isObjectChildren(identifier) && isNeedCleanup(identifier)) {
field.dispose()
delete fields[identifier]
}
})
})
}

export const isEmptyWithField = (field: GeneralField, value: any) => {
if (isArrayField(field) || isObjectField(field)) {
return isEmpty(value, true)
Expand Down
Loading

0 comments on commit 83b5f4b

Please sign in to comment.