Skip to content

Commit 911edbb

Browse files
authored
feat: Summary touched status (#215)
* chore: Init collection * fix: Recv check logic * test: Add virtual parent test * test: List test * refactor: Modify logic
1 parent fc01ed2 commit 911edbb

File tree

9 files changed

+158
-19
lines changed

9 files changed

+158
-19
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module.exports = {
44
...base,
55
rules: {
66
...base.rules,
7+
'arrow-parens': 0,
78
'no-confusing-arrow': 0,
89
'no-template-curly-in-string': 0,
910
'prefer-promise-reject-errors': 0,

.prettierrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"arrowParens": "avoid",
23
"endOfLine": "lf",
34
"semi": true,
45
"singleQuote": true,

examples/list.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ const Demo = () => {
2222
}}
2323
style={{ border: '1px solid red', padding: 15 }}
2424
preserve={false}
25+
initialValues={{
26+
users: ['little'],
27+
}}
2528
>
2629
<Form.Field shouldUpdate>{() => JSON.stringify(form.getFieldsValue(), null, 2)}</Form.Field>
2730

@@ -101,6 +104,15 @@ const Demo = () => {
101104
>
102105
Set List Value
103106
</button>
107+
108+
<button
109+
type="button"
110+
onClick={() => {
111+
console.log('`users` touched:', form.isFieldTouched('users'));
112+
}}
113+
>
114+
Is List touched
115+
</button>
104116
</div>
105117
</div>
106118
);

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"enzyme-to-json": "^3.1.4",
5454
"father": "^2.13.6",
5555
"np": "^5.0.3",
56+
"prettier": "^2.1.2",
5657
"react": "^16.14.0",
5758
"react-dnd": "^8.0.3",
5859
"react-dnd-html5-backend": "^8.0.3",

src/Field.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ export interface InternalFieldProps<Values = any> {
7777
/** @private Passed by Form.List props. Do not use since it will break by path check. */
7878
isListField?: boolean;
7979

80+
/** @private Passed by Form.List props. Do not use since it will break by path check. */
81+
isList?: boolean;
82+
8083
/** @private Pass context as prop instead of context api
8184
* since class component can not get context in constructor */
8285
fieldContext: InternalFormInstance;
@@ -361,6 +364,8 @@ class Field extends React.Component<InternalFieldProps, FieldState> implements F
361364

362365
public isListField = () => this.props.isListField;
363366

367+
public isList = () => this.props.isList;
368+
364369
// ============================= Child Component =============================
365370
public getMeta = (): Meta => {
366371
// Make error & validating in cache to save perf

src/List.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,13 @@ const List: React.FunctionComponent<ListProps> = ({ name, children, rules, valid
5454

5555
return (
5656
<FieldContext.Provider value={{ ...context, prefixName }}>
57-
<Field name={[]} shouldUpdate={shouldUpdate} rules={rules} validateTrigger={validateTrigger}>
57+
<Field
58+
name={[]}
59+
shouldUpdate={shouldUpdate}
60+
rules={rules}
61+
validateTrigger={validateTrigger}
62+
isList
63+
>
5864
{({ value = [], onChange }, meta) => {
5965
const { getFieldValue } = context;
6066
const getNewValue = () => {

src/interface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ export interface FieldEntity {
9898
isFieldDirty: () => boolean;
9999
isFieldValidating: () => boolean;
100100
isListField: () => boolean;
101+
isList: () => boolean;
101102
validateRules: (options?: ValidateOptions) => Promise<string[]>;
102103
getMeta: () => Meta;
103104
getNamePath: () => InternalNamePath;

src/useForm.ts

Lines changed: 36 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,11 @@ export class FormStore {
211211
const namePath =
212212
'INVALIDATE_NAME_PATH' in entity ? entity.INVALIDATE_NAME_PATH : entity.getNamePath();
213213

214-
// Ignore when it's a list item and not specific the namePath,
215-
// since parent field is already take in count
216-
if (!nameList && (entity as FieldEntity).isListField?.()) {
217-
return;
218-
}
214+
// Ignore when it's a list item and not specific the namePath,
215+
// since parent field is already take in count
216+
if (!nameList && (entity as FieldEntity).isListField?.()) {
217+
return;
218+
}
219219

220220
if (!filterFunc) {
221221
filteredNameList.push(namePath);
@@ -287,22 +287,41 @@ export class FormStore {
287287
isAllFieldsTouched = arg1;
288288
}
289289

290-
const testTouched = (field: FieldEntity) => {
291-
// Not provide `nameList` will check all the fields
292-
if (!namePathList) {
293-
return field.isFieldTouched();
294-
}
290+
const fieldEntities = this.getFieldEntities(true);
291+
const isFieldTouched = (field: FieldEntity) => field.isFieldTouched();
292+
293+
// ===== Will get fully compare when not config namePathList =====
294+
if (!namePathList) {
295+
return isAllFieldsTouched
296+
? fieldEntities.every(isFieldTouched)
297+
: fieldEntities.some(isFieldTouched);
298+
}
299+
300+
// Generate a nest tree for validate
301+
const map = new NameMap<FieldEntity[]>();
302+
namePathList.forEach(shortNamePath => {
303+
map.set(shortNamePath, []);
304+
});
295305

306+
fieldEntities.forEach(field => {
296307
const fieldNamePath = field.getNamePath();
297-
if (containsNamePath(namePathList, fieldNamePath)) {
298-
return field.isFieldTouched();
299-
}
300-
return isAllFieldsTouched;
301-
};
308+
309+
// Find matched entity and put into list
310+
namePathList.forEach(shortNamePath => {
311+
if (shortNamePath.every((nameUnit, i) => fieldNamePath[i] === nameUnit)) {
312+
map.update(shortNamePath, list => [...list, field]);
313+
}
314+
});
315+
});
316+
317+
// Check if NameMap value is touched
318+
const isNamePathListTouched = (entities: FieldEntity[]) => entities.some(isFieldTouched);
319+
320+
const namePathListEntities = map.map(({ value }) => value);
302321

303322
return isAllFieldsTouched
304-
? this.getFieldEntities(true).every(testTouched)
305-
: this.getFieldEntities(true).some(testTouched);
323+
? namePathListEntities.every(isNamePathListTouched)
324+
: namePathListEntities.some(isNamePathListTouched);
306325
};
307326

308327
private isFieldTouched = (name: NamePath) => {

tests/list.test.tsx

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { mount, ReactWrapper } from 'enzyme';
44
import { resetWarned } from 'rc-util/lib/warning';
55
import Form, { Field, List, FormProps } from '../src';
66
import { ListField, ListOperations, ListProps } from '../src/List';
7-
import { Meta } from '../src/interface';
7+
import { FormInstance, Meta } from '../src/interface';
88
import { Input } from './common/InfoField';
99
import { changeValue, getField } from './common';
1010
import timeout from './common/timeout';
@@ -646,4 +646,97 @@ describe('Form.List', () => {
646646
wrapper.find('button').simulate('click');
647647
expect(onValuesChange).toHaveBeenCalledWith(expect.anything(), { list: [{ first: 'light' }] });
648648
});
649+
650+
describe('isFieldTouched edge case', () => {
651+
it('virtual object', () => {
652+
const formRef = React.createRef<FormInstance>();
653+
const wrapper = mount(
654+
<Form ref={formRef}>
655+
<Form.Field name={['user', 'name']}>
656+
<Input />
657+
</Form.Field>
658+
<Form.Field name={['user', 'age']}>
659+
<Input />
660+
</Form.Field>
661+
</Form>,
662+
);
663+
664+
// Not changed
665+
expect(formRef.current.isFieldTouched('user')).toBeFalsy();
666+
expect(formRef.current.isFieldsTouched(['user'], false)).toBeFalsy();
667+
expect(formRef.current.isFieldsTouched(['user'], true)).toBeFalsy();
668+
669+
// Changed
670+
wrapper
671+
.find('input')
672+
.first()
673+
.simulate('change', { target: { value: '' } });
674+
675+
expect(formRef.current.isFieldTouched('user')).toBeTruthy();
676+
expect(formRef.current.isFieldsTouched(['user'], false)).toBeTruthy();
677+
expect(formRef.current.isFieldsTouched(['user'], true)).toBeTruthy();
678+
});
679+
680+
it('List children change', () => {
681+
const [wrapper] = generateForm(
682+
fields => (
683+
<div>
684+
{fields.map(field => (
685+
<Field {...field}>
686+
<Input />
687+
</Field>
688+
))}
689+
</div>
690+
),
691+
{
692+
initialValues: { list: ['light', 'bamboo'] },
693+
},
694+
);
695+
696+
// Not changed yet
697+
expect(form.isFieldTouched('list')).toBeFalsy();
698+
expect(form.isFieldsTouched(['list'], false)).toBeFalsy();
699+
expect(form.isFieldsTouched(['list'], true)).toBeFalsy();
700+
701+
// Change children value
702+
wrapper
703+
.find('input')
704+
.first()
705+
.simulate('change', { target: { value: 'little' } });
706+
707+
expect(form.isFieldTouched('list')).toBeTruthy();
708+
expect(form.isFieldsTouched(['list'], false)).toBeTruthy();
709+
expect(form.isFieldsTouched(['list'], true)).toBeTruthy();
710+
});
711+
712+
it('List self change', () => {
713+
const [wrapper] = generateForm((fields, opt) => (
714+
<div>
715+
{fields.map(field => (
716+
<Field {...field}>
717+
<Input />
718+
</Field>
719+
))}
720+
<button
721+
type="button"
722+
onClick={() => {
723+
opt.add();
724+
}}
725+
/>
726+
</div>
727+
));
728+
729+
// Not changed yet
730+
expect(form.isFieldTouched('list')).toBeFalsy();
731+
expect(form.isFieldsTouched(['list'], false)).toBeFalsy();
732+
expect(form.isFieldsTouched(['list'], true)).toBeFalsy();
733+
734+
// Change children value
735+
wrapper.find('button').simulate('click');
736+
737+
expect(form.isFieldTouched('list')).toBeTruthy();
738+
expect(form.isFieldsTouched(['list'], false)).toBeTruthy();
739+
expect(form.isFieldsTouched(['list'], true)).toBeTruthy();
740+
});
741+
});
649742
});

0 commit comments

Comments
 (0)