From 48aa6d574fb15f8f83551375907ebf8e2934450e Mon Sep 17 00:00:00 2001 From: janryWang Date: Fri, 5 Jul 2019 01:14:55 +0800 Subject: [PATCH 1/8] fix(@uform/utils): fix setIn with number key can not auto create array --- packages/utils/src/accessor.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/utils/src/accessor.js b/packages/utils/src/accessor.js index 7f7233a2db9..7d88bdeea7a 100644 --- a/packages/utils/src/accessor.js +++ b/packages/utils/src/accessor.js @@ -402,10 +402,10 @@ function _setIn(obj, path, value) { for (let i = 0; i < pathArr.length; i++) { const p = pathArr[i] - + const next = pathArr[i + 1] if (!isObj(obj[p])) { if (obj[p] === undefined && value === undefined) return - obj[p] = {} + obj[p] = /^\d+$/.test(next) ? [] : {} } if (i === pathArr.length - 1) { From cc72a135c01af56266951acd163a892076b2e67d Mon Sep 17 00:00:00 2001 From: janryWang Date: Fri, 5 Jul 2019 01:16:34 +0800 Subject: [PATCH 2/8] perf(@uform/utils): improve setIn performance --- packages/utils/src/accessor.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/utils/src/accessor.js b/packages/utils/src/accessor.js index 7d88bdeea7a..fd30d0976b1 100644 --- a/packages/utils/src/accessor.js +++ b/packages/utils/src/accessor.js @@ -402,10 +402,9 @@ function _setIn(obj, path, value) { for (let i = 0; i < pathArr.length; i++) { const p = pathArr[i] - const next = pathArr[i + 1] if (!isObj(obj[p])) { if (obj[p] === undefined && value === undefined) return - obj[p] = /^\d+$/.test(next) ? [] : {} + obj[p] = /^\d+$/.test(pathArr[i + 1]) ? [] : {} } if (i === pathArr.length - 1) { From 67a82e67bfebfe961ae96dec7ad830f096c1146a Mon Sep 17 00:00:00 2001 From: janryWang Date: Fri, 5 Jul 2019 01:22:50 +0800 Subject: [PATCH 3/8] test(@uform/utils): add setIn testcase --- packages/utils/src/__tests__/index.spec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/utils/src/__tests__/index.spec.js b/packages/utils/src/__tests__/index.spec.js index 930daa3cc53..d539c12b360 100644 --- a/packages/utils/src/__tests__/index.spec.js +++ b/packages/utils/src/__tests__/index.spec.js @@ -17,6 +17,20 @@ test('test accessor with large path', () => { expect(isEqual(getIn(value, 'array.0.[aa,bb]'), [123, 321])).toBeTruthy() }) +test('test setIn auto create array', () => { + const value = {} + setIn(value, 'array.0.bb.2', 'hello world') + expect( + isEqual(value, { + array: [ + { + bb: [undefined, undefined, 'hello world'] + } + ] + }) + ).toBeTruthy() +}) + test('destruct getIn', () => { // getIn 通过解构表达式从扁平数据转为复合嵌套数据 const value = { a: { b: { c: 2, d: 333 } } } From dee83d97a058eb93fdb78031f7c919757722d626 Mon Sep 17 00:00:00 2001 From: janryWang Date: Sat, 6 Jul 2019 23:41:54 +0800 Subject: [PATCH 4/8] test(react/utils): add some tests --- packages/react/src/__tests__/dynamic.spec.js | 50 ++++++++++++++++++++ packages/utils/src/__tests__/index.spec.js | 27 +++++++++++ 2 files changed, 77 insertions(+) diff --git a/packages/react/src/__tests__/dynamic.spec.js b/packages/react/src/__tests__/dynamic.spec.js index 2c57ee7d446..3bdc78df97b 100644 --- a/packages/react/src/__tests__/dynamic.spec.js +++ b/packages/react/src/__tests__/dynamic.spec.js @@ -575,3 +575,53 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn // onSubmit expect(queryAllByText('valueD-456').length).toBe(1) }) + +test('dynamic remove field and relationship needs to be retained', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'bb').subscribe(({ value }) => { + setFieldState('aa', state => { + state.visible = value === '123' + }) + }) + }} + > + + + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText, queryAllByText } = render( + + ) + expect(queryAllByTestId('input').length).toBe(4) + let removes + await sleep(33) + removes = queryAllByText('Remove Field') + fireEvent.click(removes[removes.length - 1]) + await sleep(33) + removes = queryAllByText('Remove Field') + fireEvent.click(removes[removes.length - 1]) + await sleep(33) + expect(queryAllByTestId('input').length).toBe(0) + await sleep(33) + fireEvent.click(queryByText('Add Field')) + await sleep(33) + fireEvent.click(queryByText('Add Field')) + expect(queryAllByTestId('input').length).toBe(4) + expect(queryAllByTestId('input')[1].value).toBe('123') + expect(queryAllByTestId('input')[3].value).toBe('123') +}) diff --git a/packages/utils/src/__tests__/index.spec.js b/packages/utils/src/__tests__/index.spec.js index d539c12b360..fb1e0071594 100644 --- a/packages/utils/src/__tests__/index.spec.js +++ b/packages/utils/src/__tests__/index.spec.js @@ -31,6 +31,33 @@ test('test setIn auto create array', () => { ).toBeTruthy() }) +test('test setIn dose not affect other items', () => { + const value = { + aa: [ + { + dd: [ + { + ee: '是' + } + ], + cc: '1111' + } + ] + } + + setIn(value, 'aa.1.dd.0.ee', '否') + expect( + isEqual(value.aa[0], { + dd: [ + { + ee: '是' + } + ], + cc: '1111' + }) + ).toBeTruthy() +}) + test('destruct getIn', () => { // getIn 通过解构表达式从扁平数据转为复合嵌套数据 const value = { a: { b: { c: 2, d: 333 } } } From dd529edc0597e389cf5f0c7b2142a8615f961f4d Mon Sep 17 00:00:00 2001 From: janryWang Date: Mon, 8 Jul 2019 11:27:36 +0800 Subject: [PATCH 5/8] refactor(@uform/core): code refinement --- packages/core/src/index.js | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 862bc4f98af..20923aa1c02 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -20,23 +20,20 @@ export const createForm = ({ onValidateFailed }) => { let fields = [] + let calculatedValues = caculateSchemaInitialValues( + schema, + initialValues, + ({ name, path, schemaPath }, schema, value) => { + fields.push({ name, path, schemaPath, schema, value }) + } + ) + if (isEmpty(values)) { - initialValues = caculateSchemaInitialValues( - schema, - initialValues, - ({ name, path, schemaPath }, schema, value) => { - fields.push({ name, path, schemaPath, schema, value }) - } - ) + initialValues = calculatedValues } else { - values = caculateSchemaInitialValues( - schema, - values, - ({ name, path, schemaPath }, schema, value) => { - fields.push({ name, path, schemaPath, schema, value }) - } - ) + values = calculatedValues } + const form = new Form({ initialValues, values, From 53365a16a1546484230afdf357979c3131f60010 Mon Sep 17 00:00:00 2001 From: janryWang Date: Mon, 8 Jul 2019 11:44:27 +0800 Subject: [PATCH 6/8] refactor(@uform/core): code refinement --- packages/core/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 20923aa1c02..9adc17c8b43 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -22,7 +22,7 @@ export const createForm = ({ let fields = [] let calculatedValues = caculateSchemaInitialValues( schema, - initialValues, + isEmpty(values) ? initialValues : values, ({ name, path, schemaPath }, schema, value) => { fields.push({ name, path, schemaPath, schema, value }) } From 64bf1832649bd285212c37695133a7de607ed6ab Mon Sep 17 00:00:00 2001 From: janryWang Date: Mon, 8 Jul 2019 18:51:27 +0800 Subject: [PATCH 7/8] refactor(@uform/core): code refinement --- packages/core/src/field.js | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/packages/core/src/field.js b/packages/core/src/field.js index be6d8621c69..88f92c7d8b4 100644 --- a/packages/core/src/field.js +++ b/packages/core/src/field.js @@ -64,20 +64,12 @@ export class Field { this.editable = !isEmpty(editable) ? editable : this.getContextEditable() } - if (!this.initialized) { - // sync default value in initialization - if (isEmpty(this.value) && !isEmpty(this.initialValue)) { - this.value = clone(this.initialValue) - this.context.setIn(this.name, this.value) - } - } else { - // sync default value in rereneder after removed - if (this.removed && !this.shownFromParent) { - if (!isEmpty(this.initialValue)) { - this.value = clone(this.initialValue) - this.context.setIn(this.name, this.value) - } - } + if ( + !isEmpty(this.initialValue) && + (isEmpty(this.value) || (this.removed && !this.shownFromParent)) + ) { + this.value = clone(this.initialValue) + this.context.setIn(this.name, this.value) } this.mount() From bc466e90507396df4db71275d00ae73b5ece2dc4 Mon Sep 17 00:00:00 2001 From: janrywang Date: Thu, 11 Jul 2019 16:09:23 +0800 Subject: [PATCH 8/8] fix(@uform/core/react): fix sync value --- packages/core/src/field.ts | 18 ++-- packages/core/src/form.ts | 28 +++-- packages/core/src/index.ts | 19 +++- packages/react/src/__tests__/dynamic.spec.js | 106 +++++++++++++++++++ packages/react/src/decorators/markup.tsx | 2 +- packages/react/src/state/form.tsx | 3 +- packages/types/src/field.ts | 1 + packages/types/src/form.ts | 1 + packages/utils/src/__tests__/index.spec.js | 41 +++++++ packages/utils/src/accessor.ts | 3 +- packages/utils/src/clone.ts | 1 + 11 files changed, 197 insertions(+), 26 deletions(-) diff --git a/packages/core/src/field.ts b/packages/core/src/field.ts index e2481f6e90f..561635f4c58 100644 --- a/packages/core/src/field.ts +++ b/packages/core/src/field.ts @@ -51,6 +51,8 @@ export class Field implements IField { public hiddenFromParent: boolean + public shownFromParent: boolean + public initialValue: any public namePath: string[] @@ -71,8 +73,6 @@ export class Field implements IField { private destructed: boolean - private initialized: boolean - private alreadyHiddenBeforeUnmount: boolean private fieldbrd: Broadcast @@ -94,9 +94,7 @@ export class Field implements IField { this.errors = [] this.props = {} this.effectErrors = [] - this.initialized = false this.initialize(options) - this.initialized = true } public initialize(options: IFieldOptions) { @@ -128,12 +126,12 @@ export class Field implements IField { this.editable = !isEmpty(editable) ? editable : this.getContextEditable() } - if (!this.initialized) { - if (isEmpty(this.value) && !isEmpty(this.initialValue)) { - this.value = clone(this.initialValue) - this.context.setIn(this.name, this.value) - this.context.setInitialValueIn(this.name, this.initialValue) - } + if ( + !isEmpty(this.initialValue) && + (isEmpty(this.value) || (this.removed && !this.shownFromParent)) + ) { + this.value = clone(this.initialValue) + this.context.setIn(this.name, this.value) } this.mount() diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index e41967ab447..a9e3d949093 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -43,6 +43,7 @@ type Editable = boolean | ((name: string) => boolean) const defaults = (opts: T): T => ({ initialValues: {}, + values: {}, onSubmit: (values: any) => {}, effects: ($: any) => {}, ...opts @@ -92,7 +93,10 @@ export class Form { this.updateBuffer = new BufferList() this.editable = opts.editable this.schema = opts.schema || {} - this.initialize(this.options.initialValues) + this.initialize({ + values: this.options.values, + initialValues: this.options.initialValues + }) this.initializeEffects() this.initialized = true this.destructed = false @@ -188,7 +192,7 @@ export class Form { field.initialize({ path: options.path, onChange: options.onChange, - value: !isEmpty(value) ? value : initialValue, + value, initialValue } as IFieldOptions) this.asyncUpdate(() => { @@ -197,7 +201,7 @@ export class Form { } else { this.fields[name] = new Field(this, { name, - value: !isEmpty(value) ? value : initialValue, + value, path: options.path, initialValue, props: options.props @@ -366,11 +370,13 @@ export class Form { if (field.hiddenFromParent) { field.visible = visible field.hiddenFromParent = false + field.shownFromParent = true field.dirty = true } } else { field.visible = visible field.hiddenFromParent = true + field.shownFromParent = false field.dirty = true } } @@ -526,18 +532,26 @@ export class Form { } } - public initialize(values = this.state.initialValues) { + public initialize({ + initialValues = this.state.initialValues, + values = this.state.values + }) { const lastValues = this.state.values const lastDirty = this.state.dirty + const currentInitialValues = clone(initialValues) || {} + const currentValues = isEmpty(values) + ? clone(currentInitialValues) + : clone(values) || {} this.state = { valid: true, invalid: false, errors: [], pristine: true, - initialValues: clone(values) || {}, - values: clone(values) || {}, + initialValues: currentInitialValues, + values: currentValues, dirty: - lastDirty || (this.initialized ? !isEqual(values, lastValues) : false) + lastDirty || + (this.initialized ? !isEqual(currentValues, lastValues) : false) } if (this.options.onFormChange && !this.initialized) { this.subscribe(this.options.onFormChange) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 8808730a619..6278c8855fa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,16 +1,17 @@ -import { IFormOptions } from '@uform/types' +import { IFormOptions, ISchema } from '@uform/types' import { setLocale as setValidationLocale, setLanguage as setValidationLanguage } from '@uform/validator' import { Form } from './form' -import { caculateSchemaInitialValues, isFn, each } from './utils' +import { caculateSchemaInitialValues, isFn, each, isEmpty } from './utils' export * from './path' export const createForm = ({ initialValues, + values, onSubmit, onReset, schema, @@ -23,15 +24,23 @@ export const createForm = ({ onValidateFailed }: IFormOptions) => { let fields = [] - initialValues = caculateSchemaInitialValues( + let calculatedValues = caculateSchemaInitialValues( schema, - initialValues, - ({ name, path, schemaPath }, schema, value: any) => { + isEmpty(values) ? initialValues : values, + ({ name, path, schemaPath }, schema: ISchema, value: any) => { fields.push({ name, path, schemaPath, schema, value }) } ) + + if (isEmpty(values)) { + initialValues = calculatedValues + } else { + values = calculatedValues + } + const form = new Form({ initialValues, + values, onSubmit, onReset, subscribes, diff --git a/packages/react/src/__tests__/dynamic.spec.js b/packages/react/src/__tests__/dynamic.spec.js index 2c57ee7d446..eedfd1390fc 100644 --- a/packages/react/src/__tests__/dynamic.spec.js +++ b/packages/react/src/__tests__/dynamic.spec.js @@ -575,3 +575,109 @@ test('dynamic change functions onChange/onReset/onSubmit/onValidateFailed', asyn // onSubmit expect(queryAllByText('valueD-456').length).toBe(1) }) + +test('dynamic remove field and relationship needs to be retained', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'container.*.bb').subscribe(({ value, name }) => { + const siblingName = FormPath.transform(name, /\d+/, $d => { + return `container.${$d}.aa` + }) + setFieldState(FormPath.match(siblingName), state => { + state.visible = value !== '123' + }) + }) + }} + > + + + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText, queryAllByText } = render( + + ) + expect(queryAllByTestId('input').length).toBe(2) + let removes + await sleep(33) + removes = queryAllByText('Remove Field') + fireEvent.click(removes[removes.length - 1]) + await sleep(33) + removes = queryAllByText('Remove Field') + fireEvent.click(removes[removes.length - 1]) + await sleep(33) + expect(queryAllByTestId('input').length).toBe(0) + await sleep(33) + fireEvent.click(queryByText('Add Field')) + await sleep(33) + fireEvent.click(queryByText('Add Field')) + await sleep(33) + expect(queryAllByTestId('input').length).toBe(2) + expect(queryAllByTestId('input')[0].value).toBe('123') + expect(queryAllByTestId('input')[1].value).toBe('123') +}) + +test('after deleting a component should not be sync an default value', async () => { + const TestComponent = () => { + return ( + { + $('onFieldChange', 'container.*.bb').subscribe(({ value, name }) => { + const siblingName = FormPath.transform(name, /\d+/, $d => { + return `container.${$d}.aa` + }) + setFieldState(FormPath.match(siblingName), state => { + state.visible = value === '123' + }) + }) + }} + > + + + + + + + + + ) + } + + const { queryAllByTestId, queryByText, queryAllByText } = render( + + ) + expect(queryAllByTestId('input').length).toBe(4) + let removes + await sleep(33) + removes = queryAllByText('Remove Field') + fireEvent.click(removes[removes.length - 1]) + await sleep(33) + removes = queryAllByText('Remove Field') + fireEvent.click(removes[removes.length - 1]) + await sleep(33) + expect(queryAllByTestId('input').length).toBe(0) + await sleep(33) + fireEvent.click(queryByText('Add Field')) + await sleep(33) + fireEvent.click(queryByText('Add Field')) + await sleep(33) + expect(queryAllByTestId('input').length).toBe(2) + expect(queryAllByTestId('input')[0].value).toBe('') + expect(queryAllByTestId('input')[1].value).toBe('') +}) diff --git a/packages/react/src/decorators/markup.tsx b/packages/react/src/decorators/markup.tsx index 7f6c4db679d..66e0350235c 100644 --- a/packages/react/src/decorators/markup.tsx +++ b/packages/react/src/decorators/markup.tsx @@ -81,7 +81,7 @@ export const SchemaMarkup = createHOC((options, SchemaForm) => { )} { this.initialized = false this.form = createForm({ initialValues: props.defaultValue || props.initialValues, + values: props.value, effects: props.effects, subscribes: props.subscribes, schema: props.schema, @@ -197,7 +198,7 @@ export const StateForm = createHOC((options, Form) => { !isEmpty(initialValues) && !isEqual(initialValues, prevProps.initialValues) ) { - this.form.initialize(initialValues) + this.form.initialize({ initialValues }) } if (!isEmpty(editable) && !isEqual(editable, prevProps.editable)) { this.form.changeEditable(editable) diff --git a/packages/types/src/field.ts b/packages/types/src/field.ts index c0368d1db74..063751c2f2a 100644 --- a/packages/types/src/field.ts +++ b/packages/types/src/field.ts @@ -10,6 +10,7 @@ export interface IField { invalid: boolean visible: boolean hiddenFromParent: boolean + shownFromParent: boolean required: boolean editable: boolean loading: boolean diff --git a/packages/types/src/form.ts b/packages/types/src/form.ts index a90b25e6cd3..512a99b03f2 100644 --- a/packages/types/src/form.ts +++ b/packages/types/src/form.ts @@ -36,6 +36,7 @@ export interface IFormOptions { editable: boolean | ((nam: string) => boolean) effects: IEffects defaultValue?: object + values?: object initialValues?: object schema: ISchema | {} subscribes: ISubscribers diff --git a/packages/utils/src/__tests__/index.spec.js b/packages/utils/src/__tests__/index.spec.js index 7ead232664e..ca08258f306 100644 --- a/packages/utils/src/__tests__/index.spec.js +++ b/packages/utils/src/__tests__/index.spec.js @@ -17,6 +17,47 @@ test('test accessor with large path', () => { expect(isEqual(getIn(value, 'array.0.[aa,bb]'), [123, 321])).toBeTruthy() }) +test('test setIn auto create array', () => { + const value = {} + setIn(value, 'array.0.bb.2', 'hello world') + expect( + isEqual(value, { + array: [ + { + bb: [undefined, undefined, 'hello world'] + } + ] + }) + ).toBeTruthy() +}) + +test('test setIn dose not affect other items', () => { + const value = { + aa: [ + { + dd: [ + { + ee: '是' + } + ], + cc: '1111' + } + ] + } + + setIn(value, 'aa.1.dd.0.ee', '否') + expect( + isEqual(value.aa[0], { + dd: [ + { + ee: '是' + } + ], + cc: '1111' + }) + ).toBeTruthy() +}) + test('destruct getIn', () => { // getIn 通过解构表达式从扁平数据转为复合嵌套数据 const value = { a: { b: { c: 2, d: 333 } } } diff --git a/packages/utils/src/accessor.ts b/packages/utils/src/accessor.ts index c04138fe72c..f594d9c1df7 100644 --- a/packages/utils/src/accessor.ts +++ b/packages/utils/src/accessor.ts @@ -477,12 +477,11 @@ function _setIn(obj: any, path: Path, value: any) { for (let i = 0; i < pathArr.length; i++) { const p = pathArr[i] - if (!isObj(obj[p])) { if (obj[p] === undefined && value === undefined) { return } - obj[p] = {} + obj[p] = /^\d+$/.test(pathArr[i + 1 + '']) ? [] : {} } if (i === pathArr.length - 1) { diff --git a/packages/utils/src/clone.ts b/packages/utils/src/clone.ts index adda593bf0c..23626f2bccb 100644 --- a/packages/utils/src/clone.ts +++ b/packages/utils/src/clone.ts @@ -9,6 +9,7 @@ const NATIVE_KEYS = [ ['WeakMap', (map: any) => new WeakMap(map)], ['WeakSet', (set: any) => new WeakSet(set)], ['Set', (set: any) => new Set(set)], + ['Date', (date: any) => new Date(date)], 'FileList', 'File', 'URL',