From c34908d6b4ca0b25e6c61c13d7185ff8f90dafa4 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 7 Apr 2020 15:36:29 +0800 Subject: [PATCH 1/9] add test case --- package.json | 5 +- src/Field.tsx | 1 + src/FieldContext.ts | 2 +- src/List.tsx | 2 +- src/interface.ts | 1 + src/useForm.ts | 17 ++++- src/utils/validateUtil.ts | 2 +- tests/index.test.js | 119 ----------------------------- tests/initialValue.test.js | 148 +++++++++++++++++++++++++++++++++++++ 9 files changed, 171 insertions(+), 126 deletions(-) create mode 100644 tests/initialValue.test.js diff --git a/package.json b/package.json index 67e768ba..2461d7ed 100644 --- a/package.json +++ b/package.json @@ -42,10 +42,10 @@ "react": "*" }, "devDependencies": { + "@types/jest": "^25.2.1", "@types/lodash": "^4.14.135", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.4", - "@types/warning": "^3.0.0", "enzyme": "^3.1.0", "enzyme-adapter-react-16": "^1.0.2", "enzyme-to-json": "^3.1.4", @@ -63,7 +63,6 @@ "dependencies": { "@babel/runtime": "^7.8.4", "async-validator": "^3.0.3", - "rc-util": "^4.17.0", - "warning": "^4.0.3" + "rc-util": "^4.20.3" } } diff --git a/src/Field.tsx b/src/Field.tsx index 37d2d331..061654c7 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -70,6 +70,7 @@ export interface InternalFieldProps { valuePropName?: string; getValueProps?: (value: StoreValue) => object; messageVariables?: Record; + initialValue?: any; onReset?: () => void; } diff --git a/src/FieldContext.ts b/src/FieldContext.ts index e4a30640..45f62607 100644 --- a/src/FieldContext.ts +++ b/src/FieldContext.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import warning from 'warning'; +import warning from 'rc-util/lib/warning'; import { InternalFormInstance } from './interface'; export const HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS'; diff --git a/src/List.tsx b/src/List.tsx index 1aadc1de..eb5de8a1 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import warning from 'warning'; +import warning from 'rc-util/lib/warning'; import { InternalNamePath, NamePath, StoreValue } from './interface'; import FieldContext from './FieldContext'; import Field from './Field'; diff --git a/src/interface.ts b/src/interface.ts index 2e94d903..41bd24bb 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -94,6 +94,7 @@ export interface FieldEntity { name?: NamePath; rules?: Rule[]; dependencies?: NamePath[]; + initialValue?: any; }; } diff --git a/src/useForm.ts b/src/useForm.ts index 2bbe1a35..a3f8e7b1 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -1,5 +1,5 @@ import * as React from 'react'; -import warning from 'warning'; +import warning from 'rc-util/lib/warning'; import { Callbacks, FieldData, @@ -371,6 +371,21 @@ export class FormStore { private registerField = (entity: FieldEntity) => { this.fieldEntities.push(entity); + // Set initial values + if (entity.props.initialValue !== undefined) { + const namePath = entity.getNamePath(); + const formInitialValue = getValue(this.initialValues, namePath); + if (formInitialValue !== undefined) { + warning( + false, + `Form already set 'initialValues' with path '${namePath.join( + '.', + )}'. Field can not overwrite it.`, + ); + } + } + + // un-register field callback return () => { this.fieldEntities = this.fieldEntities.filter(item => item !== entity); }; diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index bcab5a83..d91d0066 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -1,6 +1,6 @@ import RawAsyncValidator from 'async-validator'; import * as React from 'react'; -import warning from 'warning'; +import warning from 'rc-util/lib/warning'; import { InternalNamePath, ValidateOptions, diff --git a/tests/index.test.js b/tests/index.test.js index 1a9d2219..e7fdac4b 100644 --- a/tests/index.test.js +++ b/tests/index.test.js @@ -180,125 +180,6 @@ describe('Form.Basic', () => { }); }); - describe('initialValues', () => { - it('works', () => { - let form; - - const wrapper = mount( -
-
{ - form = instance; - }} - initialValues={{ username: 'Light', path1: { path2: 'Bamboo' } }} - > - - - - - - -
-
, - ); - - expect(form.getFieldsValue()).toEqual({ - username: 'Light', - path1: { - path2: 'Bamboo', - }, - }); - expect(form.getFieldsValue(['username'])).toEqual({ - username: 'Light', - }); - expect(form.getFieldsValue(['path1'])).toEqual({ - path1: { - path2: 'Bamboo', - }, - }); - expect(form.getFieldsValue(['username', ['path1', 'path2']])).toEqual({ - username: 'Light', - path1: { - path2: 'Bamboo', - }, - }); - expect( - getField(wrapper, 'username') - .find('input') - .props().value, - ).toEqual('Light'); - expect( - getField(wrapper, ['path1', 'path2']) - .find('input') - .props().value, - ).toEqual('Bamboo'); - }); - - it('update and reset should use new initialValues', () => { - let form; - let mountCount = 0; - - const TestInput = props => { - React.useEffect(() => { - mountCount += 1; - }, []); - - return ; - }; - - const Test = ({ initialValues }) => ( -
{ - form = instance; - }} - initialValues={initialValues} - > - - - - - - -
- ); - - const wrapper = mount(); - expect(form.getFieldsValue()).toEqual({ - username: 'Bamboo', - }); - expect( - getField(wrapper, 'username') - .find('input') - .props().value, - ).toEqual('Bamboo'); - - // Should not change it - wrapper.setProps({ initialValues: { username: 'Light' } }); - wrapper.update(); - expect(form.getFieldsValue()).toEqual({ - username: 'Bamboo', - }); - expect( - getField(wrapper, 'username') - .find('input') - .props().value, - ).toEqual('Bamboo'); - - // Should change it - form.resetFields(); - wrapper.update(); - expect(mountCount).toEqual(1); - expect(form.getFieldsValue()).toEqual({ - username: 'Light', - }); - expect( - getField(wrapper, 'username') - .find('input') - .props().value, - ).toEqual('Light'); - }); - }); - it('should throw if no Form in use', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); diff --git a/tests/initialValue.test.js b/tests/initialValue.test.js new file mode 100644 index 00000000..d0f7df4c --- /dev/null +++ b/tests/initialValue.test.js @@ -0,0 +1,148 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { resetWarned } from 'rc-util/lib/warning'; +import Form, { Field, useForm } from '../src'; +import InfoField, { Input } from './common/InfoField'; +import { changeValue, getField, matchError } from './common'; +import timeout from './common/timeout'; + +describe('Form.InitialValues', () => { + it('works', () => { + let form; + + const wrapper = mount( +
+
{ + form = instance; + }} + initialValues={{ username: 'Light', path1: { path2: 'Bamboo' } }} + > + + + + + + +
+
, + ); + + expect(form.getFieldsValue()).toEqual({ + username: 'Light', + path1: { + path2: 'Bamboo', + }, + }); + expect(form.getFieldsValue(['username'])).toEqual({ + username: 'Light', + }); + expect(form.getFieldsValue(['path1'])).toEqual({ + path1: { + path2: 'Bamboo', + }, + }); + expect(form.getFieldsValue(['username', ['path1', 'path2']])).toEqual({ + username: 'Light', + path1: { + path2: 'Bamboo', + }, + }); + expect( + getField(wrapper, 'username') + .find('input') + .props().value, + ).toEqual('Light'); + expect( + getField(wrapper, ['path1', 'path2']) + .find('input') + .props().value, + ).toEqual('Bamboo'); + }); + + it('update and reset should use new initialValues', () => { + let form; + let mountCount = 0; + + const TestInput = props => { + React.useEffect(() => { + mountCount += 1; + }, []); + + return ; + }; + + const Test = ({ initialValues }) => ( +
{ + form = instance; + }} + initialValues={initialValues} + > + + + + + + +
+ ); + + const wrapper = mount(); + expect(form.getFieldsValue()).toEqual({ + username: 'Bamboo', + }); + expect( + getField(wrapper, 'username') + .find('input') + .props().value, + ).toEqual('Bamboo'); + + // Should not change it + wrapper.setProps({ initialValues: { username: 'Light' } }); + wrapper.update(); + expect(form.getFieldsValue()).toEqual({ + username: 'Bamboo', + }); + expect( + getField(wrapper, 'username') + .find('input') + .props().value, + ).toEqual('Bamboo'); + + // Should change it + form.resetFields(); + wrapper.update(); + expect(mountCount).toEqual(1); + expect(form.getFieldsValue()).toEqual({ + username: 'Light', + }); + expect( + getField(wrapper, 'username') + .find('input') + .props().value, + ).toEqual('Light'); + }); + + describe('Field with initialValue', () => { + it('warning if Form already has', () => { + resetWarned(); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + const wrapper = mount( +
+ + + +
, + ); + + expect(wrapper.find('input').props().value).toEqual('bamboo'); + + expect(errorSpy).toHaveBeenCalledWith( + "Warning: Form already set 'initialValues' with path 'conflict'. Field can not overwrite it.", + ); + + errorSpy.mockRestore(); + }); + }); +}); From e95c93dfeacb599b064ffc2c559c1e211bf02a66 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 7 Apr 2020 16:03:26 +0800 Subject: [PATCH 2/9] logic it --- src/useForm.ts | 95 ++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 10 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index a3f8e7b1..c600b63c 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -308,12 +308,95 @@ export class FormStore { return this.isFieldsValidating([name]); }; + /** + * Reset Field with field `initialValue` prop. + * Can pass `entities` or `namePathList` or just nothing. + */ + private resetWithFieldInitialValue = ( + info: { + entities?: FieldEntity[]; + namePathList?: InternalNamePath[]; + } = {}, + ) => { + // Create cache + const cache: NameMap> = new NameMap(); + + const fieldEntities = this.getFieldEntities(true); + fieldEntities.forEach(field => { + const { initialValue } = field.props; + const namePath = field.getNamePath(); + + // Record only if has `initialValue` + if (initialValue !== undefined) { + const records = cache.get(namePath) || new Set(); + records.add({ entity: field, value: initialValue }); + + cache.set(namePath, records); + } + }); + + // Reset + const resetWithFields = (entities: FieldEntity[]) => { + entities.forEach(field => { + const { initialValue } = field.props; + + if (initialValue !== undefined) { + const namePath = field.getNamePath(); + const formInitialValue = this.getInitialValue(namePath); + + if (formInitialValue !== undefined) { + // Warning if conflict with form initialValues and do not modify value + warning( + false, + `Form already set 'initialValues' with path '${namePath.join( + '.', + )}'. Field can not overwrite it.`, + ); + } else { + const records = cache.get(namePath); + if (records && records.size > 1) { + // Warning if multiple field set `initialValue`and do not modify value + warning( + false, + `Multiple Field with path '${namePath.join( + '.', + )}' set 'initialValue'. Can not decide which one to pick.`, + ); + } else if (records) { + // Set `initialValue` + this.store = setValue(this.store, namePath, [...records][0].value); + } + } + } + }); + }; + + let requiredFieldEntities: FieldEntity[]; + if (info.entities) { + requiredFieldEntities = info.entities; + } else if (info.namePathList) { + requiredFieldEntities = []; + + info.namePathList.forEach(namePath => { + const records = cache.get(namePath); + if (records) { + requiredFieldEntities.push(...[...records].map(r => r.entity)); + } + }); + } else { + requiredFieldEntities = fieldEntities; + } + + resetWithFields(requiredFieldEntities); + }; + private resetFields = (nameList?: NamePath[]) => { this.warningUnhooked(); const prevStore = this.store; if (!nameList) { this.store = setValues({}, this.initialValues); + this.resetWithFieldInitialValue(); this.notifyObservers(prevStore, null, { type: 'reset' }); return; } @@ -324,6 +407,7 @@ export class FormStore { const initialValue = this.getInitialValue(namePath); this.store = setValue(this.store, namePath, initialValue); }); + this.resetWithFieldInitialValue({ namePathList }); this.notifyObservers(prevStore, namePathList, { type: 'reset' }); }; @@ -373,16 +457,7 @@ export class FormStore { // Set initial values if (entity.props.initialValue !== undefined) { - const namePath = entity.getNamePath(); - const formInitialValue = getValue(this.initialValues, namePath); - if (formInitialValue !== undefined) { - warning( - false, - `Form already set 'initialValues' with path '${namePath.join( - '.', - )}'. Field can not overwrite it.`, - ); - } + this.resetWithFieldInitialValue({ entities: [entity] }); } // un-register field callback From cc9ff8d170b13e5b5606b5dbefee2e7cd6a3d210 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 7 Apr 2020 16:24:58 +0800 Subject: [PATCH 3/9] update set logic --- src/useForm.ts | 9 +++++++-- tests/initialValue.test.js | 23 ++++++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index c600b63c..ef08ff18 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -316,6 +316,8 @@ export class FormStore { info: { entities?: FieldEntity[]; namePathList?: InternalNamePath[]; + /** Skip reset if store exist value. This is only used for field register reset */ + skipExist?: boolean; } = {}, ) => { // Create cache @@ -363,8 +365,11 @@ export class FormStore { )}' set 'initialValue'. Can not decide which one to pick.`, ); } else if (records) { + const originValue = this.getFieldValue(namePath); // Set `initialValue` - this.store = setValue(this.store, namePath, [...records][0].value); + if (!info.skipExist || originValue === undefined) { + this.store = setValue(this.store, namePath, [...records][0].value); + } } } } @@ -457,7 +462,7 @@ export class FormStore { // Set initial values if (entity.props.initialValue !== undefined) { - this.resetWithFieldInitialValue({ entities: [entity] }); + this.resetWithFieldInitialValue({ entities: [entity], skipExist: true }); } // un-register field callback diff --git a/tests/initialValue.test.js b/tests/initialValue.test.js index d0f7df4c..894996d4 100644 --- a/tests/initialValue.test.js +++ b/tests/initialValue.test.js @@ -125,7 +125,7 @@ describe('Form.InitialValues', () => { }); describe('Field with initialValue', () => { - it('warning if Form already has', () => { + it('warning if Form already has initialValues', () => { resetWarned(); const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); const wrapper = mount( @@ -144,5 +144,26 @@ describe('Form.InitialValues', () => { errorSpy.mockRestore(); }); + + it('warning if multiple Field with same name set `initialValue`', () => { + resetWarned(); + const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + mount( +
+ + + + + + +
, + ); + + expect(errorSpy).toHaveBeenCalledWith( + "Warning: Multiple Field with path 'conflict' set 'initialValue'. Can not decide which one to pick.", + ); + + errorSpy.mockRestore(); + }); }); }); From 2f2b0a6dec155728ba25054e55fabd0fe9e8099d Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 7 Apr 2020 16:33:40 +0800 Subject: [PATCH 4/9] Fix refresh logic --- src/useForm.ts | 5 +++++ tests/initialValue.test.js | 35 ++++++++++++++++++++++++++++++++--- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index ef08ff18..9d8b581b 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -462,7 +462,12 @@ export class FormStore { // Set initial values if (entity.props.initialValue !== undefined) { + const prevStore = this.store; this.resetWithFieldInitialValue({ entities: [entity], skipExist: true }); + this.notifyObservers(prevStore, [entity.getNamePath()], { + type: 'valueUpdate', + source: 'internal', + }); } // un-register field callback diff --git a/tests/initialValue.test.js b/tests/initialValue.test.js index 894996d4..834f9bc3 100644 --- a/tests/initialValue.test.js +++ b/tests/initialValue.test.js @@ -131,7 +131,7 @@ describe('Form.InitialValues', () => { const wrapper = mount(
- +
, ); @@ -151,10 +151,10 @@ describe('Form.InitialValues', () => { mount(
- + - +
, ); @@ -165,5 +165,34 @@ describe('Form.InitialValues', () => { errorSpy.mockRestore(); }); + + it('should not replace user input', () => { + const Test = () => { + const [show, setShow] = React.useState(false); + + return ( +
+ {show && ( + + + + )} +