diff --git a/README.md b/README.md index 3e426ddc..9b9b1feb 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ We use typescript to create the Type definition. You can view directly in IDE. B | dependencies | Will re-render if dependencies changed | [NamePath](#namepath)[] | - | | getValueFromEvent | Specify how to get value from event | (..args: any[]) => any | - | | getValueProps | Customize additional props with value. This prop will disable `valuePropName` | (value) => any | - | +| initialValue | Field initial value | any | - | | name | Field name path | [NamePath](#namepath) | - | | normalize | Normalize value before update | (value, prevValue, prevValues) => any | - | | rules | Validate rules | [Rule](#rule)[] | - | 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..9d8b581b 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, @@ -308,12 +308,100 @@ 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[]; + /** Skip reset if store exist value. This is only used for field register reset */ + skipExist?: boolean; + } = {}, + ) => { + // 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) { + const originValue = this.getFieldValue(namePath); + // Set `initialValue` + if (!info.skipExist || originValue === undefined) { + 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 +412,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' }); }; @@ -371,6 +460,17 @@ export class FormStore { private registerField = (entity: FieldEntity) => { this.fieldEntities.push(entity); + // 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 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..e5f77e4a --- /dev/null +++ b/tests/initialValue.test.js @@ -0,0 +1,310 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import { resetWarned } from 'rc-util/lib/warning'; +import Form, { Field, useForm } from '../src'; +import { Input } from './common/InfoField'; +import { changeValue, getField } from './common'; + +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 initialValues', () => { + 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(); + }); + + 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(); + }); + + it('should not replace user input', async () => { + const Test = () => { + const [show, setShow] = React.useState(false); + + return ( +
+ {show && ( + + + + )} +