From 8c88866e0360a1f4a1c759cba16568bdded00509 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 6 Nov 2020 17:07:50 +0800 Subject: [PATCH 1/5] chore: Init collection --- .eslintrc.js | 1 + .prettierrc | 1 + examples/list.tsx | 12 ++++++++++++ package.json | 1 + src/useForm.ts | 46 ++++++++++++++++++++++++++++------------------ 5 files changed, 43 insertions(+), 18 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 799bcd20..b783ff93 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -4,6 +4,7 @@ module.exports = { ...base, rules: { ...base.rules, + 'arrow-parens': 0, 'no-confusing-arrow': 0, 'no-template-curly-in-string': 0, 'prefer-promise-reject-errors': 0, diff --git a/.prettierrc b/.prettierrc index f307fb19..60e6c298 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,5 @@ { + "arrowParens": "avoid", "endOfLine": "lf", "semi": true, "singleQuote": true, diff --git a/examples/list.tsx b/examples/list.tsx index 16e5aed9..ef70f70e 100644 --- a/examples/list.tsx +++ b/examples/list.tsx @@ -22,6 +22,9 @@ const Demo = () => { }} style={{ border: '1px solid red', padding: 15 }} preserve={false} + initialValues={{ + users: ['little'], + }} > {() => JSON.stringify(form.getFieldsValue(), null, 2)} @@ -101,6 +104,15 @@ const Demo = () => { > Set List Value + + ); diff --git a/package.json b/package.json index 0fca592d..45027bdf 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "enzyme-to-json": "^3.1.4", "father": "^2.13.6", "np": "^5.0.3", + "prettier": "^2.1.2", "react": "^16.14.0", "react-dnd": "^8.0.3", "react-dnd-html5-backend": "^8.0.3", diff --git a/src/useForm.ts b/src/useForm.ts index a05d4a40..fa801247 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -211,11 +211,11 @@ export class FormStore { const namePath = 'INVALIDATE_NAME_PATH' in entity ? entity.INVALIDATE_NAME_PATH : entity.getNamePath(); - // Ignore when it's a list item and not specific the namePath, - // since parent field is already take in count - if (!nameList && (entity as FieldEntity).isListField?.()) { - return; - } + // Ignore when it's a list item and not specific the namePath, + // since parent field is already take in count + if (!nameList && (entity as FieldEntity).isListField?.()) { + return; + } if (!filterFunc) { filteredNameList.push(namePath); @@ -287,22 +287,32 @@ export class FormStore { isAllFieldsTouched = arg1; } - const testTouched = (field: FieldEntity) => { - // Not provide `nameList` will check all the fields - if (!namePathList) { - return field.isFieldTouched(); - } + const fieldEntities = this.getFieldEntities(true); + + // Will get fully compare when not config namePathList + if (!namePathList) { + const isFieldTouched = (field: FieldEntity) => field.isFieldTouched(); + return isAllFieldsTouched + ? fieldEntities.every(isFieldTouched) + : fieldEntities.some(isFieldTouched); + } + // Generate a nest tree for validate + const map = new NameMap(); + namePathList.forEach(shortNamePath => { + map.set(shortNamePath, []); + }); + + fieldEntities.forEach(field => { const fieldNamePath = field.getNamePath(); - if (containsNamePath(namePathList, fieldNamePath)) { - return field.isFieldTouched(); - } - return isAllFieldsTouched; - }; - return isAllFieldsTouched - ? this.getFieldEntities(true).every(testTouched) - : this.getFieldEntities(true).some(testTouched); + // Find matched entity and put into list + namePathList.forEach(shortNamePath => { + if (shortNamePath.every((nameUnit, i) => fieldNamePath[i] === nameUnit)) { + map.update(shortNamePath, list => [...list, field]); + } + }); + }); }; private isFieldTouched = (name: NamePath) => { From cb894205a82b78029da1d43d7299a1bf44e8a319 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 6 Nov 2020 17:20:55 +0800 Subject: [PATCH 2/5] fix: Recv check logic --- src/Field.tsx | 5 +++++ src/List.tsx | 8 +++++++- src/interface.ts | 1 + src/useForm.ts | 12 ++++++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Field.tsx b/src/Field.tsx index 301737f3..a05c0888 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -77,6 +77,9 @@ export interface InternalFieldProps { /** @private Passed by Form.List props. Do not use since it will break by path check. */ isListField?: boolean; + /** @private Passed by Form.List props. Do not use since it will break by path check. */ + isList?: boolean; + /** @private Pass context as prop instead of context api * since class component can not get context in constructor */ fieldContext: InternalFormInstance; @@ -361,6 +364,8 @@ class Field extends React.Component implements F public isListField = () => this.props.isListField; + public isList = () => this.props.isList; + // ============================= Child Component ============================= public getMeta = (): Meta => { // Make error & validating in cache to save perf diff --git a/src/List.tsx b/src/List.tsx index 26a66d88..30694969 100644 --- a/src/List.tsx +++ b/src/List.tsx @@ -54,7 +54,13 @@ const List: React.FunctionComponent = ({ name, children, rules, valid return ( - + {({ value = [], onChange }, meta) => { const { getFieldValue } = context; const getNewValue = () => { diff --git a/src/interface.ts b/src/interface.ts index e29c2020..433ebe5e 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -98,6 +98,7 @@ export interface FieldEntity { isFieldDirty: () => boolean; isFieldValidating: () => boolean; isListField: () => boolean; + isList: () => boolean; validateRules: (options?: ValidateOptions) => Promise; getMeta: () => Meta; getNamePath: () => InternalNamePath; diff --git a/src/useForm.ts b/src/useForm.ts index fa801247..cc7c1a45 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -288,10 +288,10 @@ export class FormStore { } const fieldEntities = this.getFieldEntities(true); + const isFieldTouched = (field: FieldEntity) => field.isFieldTouched(); - // Will get fully compare when not config namePathList + // ===== Will get fully compare when not config namePathList ===== if (!namePathList) { - const isFieldTouched = (field: FieldEntity) => field.isFieldTouched(); return isAllFieldsTouched ? fieldEntities.every(isFieldTouched) : fieldEntities.some(isFieldTouched); @@ -313,6 +313,14 @@ export class FormStore { } }); }); + + // Check if NameMap value is touched + const isNamePathListTouched = (entities: FieldEntity[]) => + isAllFieldsTouched ? entities.every(isFieldTouched) : entities.some(isFieldTouched); + + const namePathListEntities = map.map(({ value }) => value); + + return isAllFieldsTouched ? namePathListEntities.every(isNamePathListTouched) : namePathListEntities.some(isNamePathListTouched); }; private isFieldTouched = (name: NamePath) => { From 173825cfaa284768aaa9aea78c058c4fb01b7da9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 6 Nov 2020 17:39:27 +0800 Subject: [PATCH 3/5] test: Add virtual parent test --- src/useForm.ts | 6 ++++-- tests/list.test.tsx | 50 +++++++++++++++++++++++++++------------------ 2 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index cc7c1a45..cd87c382 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -318,9 +318,11 @@ export class FormStore { const isNamePathListTouched = (entities: FieldEntity[]) => isAllFieldsTouched ? entities.every(isFieldTouched) : entities.some(isFieldTouched); - const namePathListEntities = map.map(({ value }) => value); + const namePathListEntities = map.map(({ value }) => value); - return isAllFieldsTouched ? namePathListEntities.every(isNamePathListTouched) : namePathListEntities.some(isNamePathListTouched); + return isAllFieldsTouched + ? namePathListEntities.every(isNamePathListTouched) + : namePathListEntities.some(isNamePathListTouched); }; private isFieldTouched = (name: NamePath) => { diff --git a/tests/list.test.tsx b/tests/list.test.tsx index b0ab29b3..ec68ef20 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -4,7 +4,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { resetWarned } from 'rc-util/lib/warning'; import Form, { Field, List, FormProps } from '../src'; import { ListField, ListOperations, ListProps } from '../src/List'; -import { Meta } from '../src/interface'; +import { FormInstance, Meta } from '../src/interface'; import { Input } from './common/InfoField'; import { changeValue, getField } from './common'; import timeout from './common/timeout'; @@ -58,12 +58,7 @@ describe('Form.List', () => { ); function matchKey(index, key) { - expect( - getList() - .find(Field) - .at(index) - .key(), - ).toEqual(key); + expect(getList().find(Field).at(index).key()).toEqual(key); } matchKey(0, '0'); @@ -117,12 +112,7 @@ describe('Form.List', () => { }); function matchKey(index, key) { - expect( - getList() - .find(Field) - .at(index) - .key(), - ).toEqual(key); + expect(getList().find(Field).at(index).key()).toEqual(key); } // Add @@ -267,12 +257,7 @@ describe('Form.List', () => { }); function matchKey(index, key) { - expect( - getList() - .find(Field) - .at(index) - .key(), - ).toEqual(key); + expect(getList().find(Field).at(index).key()).toEqual(key); } act(() => { @@ -533,7 +518,7 @@ describe('Form.List', () => { it('warning if children is not function', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - generateForm(
as any); + generateForm((
) as any); expect(errorSpy).toHaveBeenCalledWith('Warning: Form.List only accepts function as children.'); @@ -646,4 +631,29 @@ describe('Form.List', () => { wrapper.find('button').simulate('click'); expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { list: [{ first: 'light' }] }); }); + + describe('isFieldTouched edge case', () => { + it('virtual object', () => { + const formRef = React.createRef(); + const wrapper = mount( +
+ + + + + + +
, + ); + + wrapper + .find('input') + .first() + .simulate('change', { target: { value: '' } }); + + expect(formRef.current.isFieldTouched('user')).toBeTruthy(); + expect(formRef.current.isFieldsTouched(['user'], false)).toBeTruthy(); + expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy(); + }); + }); }); From 5106c97405652a34700d2a3efa953a9977060051 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 6 Nov 2020 17:45:57 +0800 Subject: [PATCH 4/5] test: List test --- tests/list.test.tsx | 101 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 92 insertions(+), 9 deletions(-) diff --git a/tests/list.test.tsx b/tests/list.test.tsx index ec68ef20..abf661d5 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -58,7 +58,12 @@ describe('Form.List', () => { ); function matchKey(index, key) { - expect(getList().find(Field).at(index).key()).toEqual(key); + expect( + getList() + .find(Field) + .at(index) + .key(), + ).toEqual(key); } matchKey(0, '0'); @@ -112,7 +117,12 @@ describe('Form.List', () => { }); function matchKey(index, key) { - expect(getList().find(Field).at(index).key()).toEqual(key); + expect( + getList() + .find(Field) + .at(index) + .key(), + ).toEqual(key); } // Add @@ -257,7 +267,12 @@ describe('Form.List', () => { }); function matchKey(index, key) { - expect(getList().find(Field).at(index).key()).toEqual(key); + expect( + getList() + .find(Field) + .at(index) + .key(), + ).toEqual(key); } act(() => { @@ -518,7 +533,7 @@ describe('Form.List', () => { it('warning if children is not function', () => { const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - generateForm((
) as any); + generateForm(
as any); expect(errorSpy).toHaveBeenCalledWith('Warning: Form.List only accepts function as children.'); @@ -638,22 +653,90 @@ describe('Form.List', () => { const wrapper = mount(
- + - +
, ); + // Not changed + expect(formRef.current.isFieldTouched('user')).toBeFalsy(); + expect(formRef.current.isFieldsTouched(['user'], false)).toBeFalsy(); + expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy(); + + // Changed wrapper .find('input') .first() .simulate('change', { target: { value: '' } }); - expect(formRef.current.isFieldTouched('user')).toBeTruthy(); - expect(formRef.current.isFieldsTouched(['user'], false)).toBeTruthy(); - expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy(); + expect(formRef.current.isFieldTouched('user')).toBeTruthy(); + expect(formRef.current.isFieldsTouched(['user'], false)).toBeTruthy(); + expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy(); + }); + + it('List children change', () => { + const [wrapper] = generateForm( + fields => ( +
+ {fields.map(field => ( + + + + ))} +
+ ), + { + initialValues: { list: ['light', 'bamboo'] }, + }, + ); + + // Not changed yet + expect(form.isFieldTouched('list')).toBeFalsy(); + expect(form.isFieldsTouched(['list'], false)).toBeFalsy(); + expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + + // Change children value + wrapper + .find('input') + .first() + .simulate('change', { target: { value: 'little' } }); + + expect(form.isFieldTouched('list')).toBeTruthy(); + expect(form.isFieldsTouched(['list'], false)).toBeTruthy(); + expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + }); + + it('List self change', () => { + const [wrapper] = generateForm((fields, opt) => ( +
+ {fields.map(field => ( + + + + ))} +
+ )); + + // Not changed yet + expect(form.isFieldTouched('list')).toBeFalsy(); + expect(form.isFieldsTouched(['list'], false)).toBeFalsy(); + expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + + // Change children value + wrapper.find('button').simulate('click'); + + expect(form.isFieldTouched('list')).toBeTruthy(); + expect(form.isFieldsTouched(['list'], false)).toBeTruthy(); + expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); }); }); }); From b985a095622617eaeac2f2cf999ba36aae18e5ba Mon Sep 17 00:00:00 2001 From: zombiej Date: Sun, 8 Nov 2020 20:30:31 +0800 Subject: [PATCH 5/5] refactor: Modify logic --- src/useForm.ts | 3 +-- tests/list.test.tsx | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/useForm.ts b/src/useForm.ts index cd87c382..1f90086d 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -315,8 +315,7 @@ export class FormStore { }); // Check if NameMap value is touched - const isNamePathListTouched = (entities: FieldEntity[]) => - isAllFieldsTouched ? entities.every(isFieldTouched) : entities.some(isFieldTouched); + const isNamePathListTouched = (entities: FieldEntity[]) => entities.some(isFieldTouched); const namePathListEntities = map.map(({ value }) => value); diff --git a/tests/list.test.tsx b/tests/list.test.tsx index abf661d5..7f25e90f 100644 --- a/tests/list.test.tsx +++ b/tests/list.test.tsx @@ -674,7 +674,7 @@ describe('Form.List', () => { expect(formRef.current.isFieldTouched('user')).toBeTruthy(); expect(formRef.current.isFieldsTouched(['user'], false)).toBeTruthy(); - expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy(); + expect(formRef.current.isFieldsTouched(['user'], true)).toBeTruthy(); }); it('List children change', () => { @@ -706,7 +706,7 @@ describe('Form.List', () => { expect(form.isFieldTouched('list')).toBeTruthy(); expect(form.isFieldsTouched(['list'], false)).toBeTruthy(); - expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + expect(form.isFieldsTouched(['list'], true)).toBeTruthy(); }); it('List self change', () => { @@ -736,7 +736,7 @@ describe('Form.List', () => { expect(form.isFieldTouched('list')).toBeTruthy(); expect(form.isFieldsTouched(['list'], false)).toBeTruthy(); - expect(form.isFieldsTouched(['list'], true)).toBeFalsy(); + expect(form.isFieldsTouched(['list'], true)).toBeTruthy(); }); }); });