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/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 a05d4a40..1f90086d 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,41 @@ 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); + const isFieldTouched = (field: FieldEntity) => field.isFieldTouched(); + + // ===== Will get fully compare when not config namePathList ===== + if (!namePathList) { + 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; - }; + + // Find matched entity and put into list + namePathList.forEach(shortNamePath => { + if (shortNamePath.every((nameUnit, i) => fieldNamePath[i] === nameUnit)) { + map.update(shortNamePath, list => [...list, field]); + } + }); + }); + + // Check if NameMap value is touched + const isNamePathListTouched = (entities: FieldEntity[]) => entities.some(isFieldTouched); + + const namePathListEntities = map.map(({ value }) => value); return isAllFieldsTouched - ? this.getFieldEntities(true).every(testTouched) - : this.getFieldEntities(true).some(testTouched); + ? namePathListEntities.every(isNamePathListTouched) + : namePathListEntities.some(isNamePathListTouched); }; private isFieldTouched = (name: NamePath) => { diff --git a/tests/list.test.tsx b/tests/list.test.tsx index b0ab29b3..7f25e90f 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'; @@ -646,4 +646,97 @@ 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( +
+ + + + + + +
, + ); + + // 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)).toBeTruthy(); + }); + + 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)).toBeTruthy(); + }); + + 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)).toBeTruthy(); + }); + }); });