Skip to content

Commit 4d8745d

Browse files
committed
re-design list logic
1 parent 34c394c commit 4d8745d

16 files changed

+261
-222
lines changed

examples/StateForm-list.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ const Demo = () => {
2424
>
2525
<List name="users">
2626
{(fields, { add, remove }) => {
27+
console.log('Demo Fields:', fields);
2728
return (
2829
<div>
2930
<h4>List of `users`</h4>
3031
{fields.map((field, index) => (
31-
<LabelField {...field}>
32+
<LabelField {...field} rules={[{ required: true }]}>
3233
{control => (
3334
<div style={{ position: 'relative' }}>
3435
<Input {...control} />

src/Field.tsx

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import {
2323
defaultGetValueFromEvent,
2424
getNamePath,
2525
getValue,
26-
isSimilar,
2726
} from './utils/valueUtil';
2827

2928
interface ChildProps {
@@ -84,11 +83,10 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
8483

8584
private validatePromise: Promise<string[]> | null = null;
8685

87-
// We reuse the promise to check if is `validating`
88-
private prevErrors: string[];
89-
9086
private prevValidating: boolean;
9187

88+
private errors: string[] = [];
89+
9290
// ============================== Subscriptions ==============================
9391
public componentDidMount() {
9492
const { getInternalHooks }: InternalFormInstance = this.context;
@@ -159,7 +157,7 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
159157
info: NotifyInfo,
160158
) => {
161159
const { shouldUpdate, dependencies = [], onReset } = this.props;
162-
const { getFieldsValue, getFieldError }: FormInstance = this.context;
160+
const { getFieldsValue }: FormInstance = this.context;
163161
const values = getFieldsValue();
164162
const namePath = this.getNamePath();
165163
const prevValue = this.getValue(prevStore);
@@ -171,6 +169,7 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
171169
// Clean up state
172170
this.touched = false;
173171
this.validatePromise = null;
172+
this.errors = [];
174173

175174
if (onReset) {
176175
onReset();
@@ -190,24 +189,16 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
190189
if ('validating' in data) {
191190
this.validatePromise = data.validating ? Promise.resolve([]) : null;
192191
}
192+
if ('errors' in data) {
193+
this.errors = data.errors || [];
194+
}
193195

194196
this.refresh();
195197
return;
196198
}
197199
break;
198200
}
199201

200-
case 'errorUpdate': {
201-
const errors = getFieldError(namePath);
202-
const validating = this.isFieldValidating();
203-
204-
if (this.prevValidating !== validating || !isSimilar(this.prevErrors, errors)) {
205-
this.reRender();
206-
return;
207-
}
208-
break;
209-
}
210-
211202
case 'dependenciesUpdate': {
212203
/**
213204
* Trigger when marked `dependencies` updated. Related fields will all update
@@ -250,8 +241,6 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
250241
}
251242
};
252243

253-
public isFieldTouched = () => this.touched;
254-
255244
public validateRules = (options?: ValidateOptions) => {
256245
const { triggerName } = (options || {}) as ValidateOptions;
257246
const namePath = this.getNamePath();
@@ -270,12 +259,15 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
270259

271260
const promise = validateRules(namePath, this.getValue(), filteredRules, options);
272261
this.validatePromise = promise;
262+
this.errors = [];
273263

274264
promise
275265
.catch(e => e)
276-
.then(() => {
266+
.then((errors: string[] = []) => {
277267
if (this.validatePromise === promise) {
278268
this.validatePromise = null;
269+
this.errors = errors;
270+
this.reRender();
279271
}
280272
});
281273

@@ -284,17 +276,19 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
284276

285277
public isFieldValidating = () => !!this.validatePromise;
286278

279+
public isFieldTouched = () => this.touched;
280+
281+
public getErrors = () => this.errors;
282+
287283
// ============================= Child Component =============================
288284
public getMeta = (): Meta => {
289-
const { getFieldError } = this.context;
290285
// Make error & validating in cache to save perf
291286
this.prevValidating = this.isFieldValidating();
292-
this.prevErrors = getFieldError(this.getNamePath());
293287

294288
const meta: Meta = {
295289
touched: this.isFieldTouched(),
296290
validating: this.prevValidating,
297-
errors: this.prevErrors,
291+
errors: this.errors,
298292
};
299293

300294
return meta;

src/Form.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import useForm from './useForm';
1111
import FieldContext, { HOOK_MARK } from './FieldContext';
1212
import FormContext, { FormContextProps } from './FormContext';
13+
import { isSimilar } from './utils/valueUtil';
1314

1415
type BaseFormProps = Omit<React.FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
1516

@@ -105,10 +106,12 @@ const Form: React.FunctionComponent<FormProps> = (
105106

106107
// Listen if fields provided. We use ref to save prev data here to avoid additional render
107108
const prevFieldsRef = React.useRef<FieldData[] | undefined>();
108-
if (prevFieldsRef.current !== fields) {
109-
formInstance.setFields(fields || []);
110-
}
111-
prevFieldsRef.current = fields;
109+
React.useEffect(() => {
110+
if (!isSimilar(prevFieldsRef.current || [], fields || [])) {
111+
formInstance.setFields(fields || []);
112+
}
113+
prevFieldsRef.current = fields;
114+
}, [fields, formInstance]);
112115

113116
if (
114117
__COMPATIBILITY_USAGE_OR_YOU_WILL_BE_FIRED__ &&

src/List.tsx

Lines changed: 79 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import * as React from 'react';
22
import warning from 'warning';
3-
import { InternalNamePath, NamePath, InternalFormInstance, StoreValue } from './interface';
4-
import FieldContext, { HOOK_MARK } from './FieldContext';
3+
import { InternalNamePath, NamePath, StoreValue } from './interface';
4+
import FieldContext from './FieldContext';
55
import Field from './Field';
6-
import { getNamePath, setValue } from './utils/valueUtil';
6+
import { getNamePath } from './utils/valueUtil';
77

88
interface ListField {
99
name: number;
@@ -21,85 +21,90 @@ interface ListProps {
2121
}
2222

2323
const List: React.FunctionComponent<ListProps> = ({ name, children }) => {
24+
const context = React.useContext(FieldContext);
25+
const keyRef = React.useRef({
26+
keys: [],
27+
id: 0,
28+
});
29+
const keyManager = keyRef.current;
30+
2431
// User should not pass `children` as other type.
2532
if (typeof children !== 'function') {
2633
warning(false, 'Form.List only accepts function as children.');
2734
return null;
2835
}
2936

37+
const parentPrefixName = getNamePath(context.prefixName) || [];
38+
const prefixName: InternalNamePath = [...parentPrefixName, ...getNamePath(name)];
39+
40+
const shouldUpdate = (prevValue: StoreValue, nextValue: StoreValue, { source }) => {
41+
if (source === 'internal') {
42+
return false;
43+
}
44+
return prevValue !== nextValue;
45+
};
46+
3047
return (
31-
<FieldContext.Consumer>
32-
{(context: InternalFormInstance) => {
33-
const parentPrefixName = getNamePath(context.prefixName) || [];
34-
const prefixName: InternalNamePath = [...parentPrefixName, ...getNamePath(name)];
35-
36-
const shouldUpdate = (prevValue: StoreValue, nextValue: StoreValue, { source }) => {
37-
if (source === 'internal') {
38-
return false;
39-
}
40-
return prevValue !== nextValue;
41-
};
42-
43-
return (
44-
<FieldContext.Provider value={{ ...context, prefixName }}>
45-
<Field name={[]} shouldUpdate={shouldUpdate}>
46-
{({ value = [], onChange }) => {
47-
const { getInternalHooks, getFieldValue, setFieldsValue, setFields } = context;
48-
49-
/**
50-
* Always get latest value in case user update fields by `form` api.
51-
*/
52-
const operations: ListOperations = {
53-
add: () => {
54-
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
55-
onChange([...newValue, undefined]);
56-
},
57-
remove: (index: number) => {
58-
const { getFields } = getInternalHooks(HOOK_MARK);
59-
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
60-
const namePathList: InternalNamePath[] = newValue.map((__, i) => [
61-
...prefixName,
62-
i,
63-
]);
64-
65-
const fields = getFields(namePathList)
66-
.filter((__, i) => i !== index)
67-
.map((fieldData, i) => ({
68-
...fieldData,
69-
name: [...prefixName, i],
70-
}));
71-
72-
const nextValue = [...newValue];
73-
nextValue.splice(index, 1);
74-
75-
setFieldsValue(setValue({}, prefixName, []));
76-
77-
// Set value back.
78-
// We should add current list name also to let it re-render
79-
setFields([
80-
...fields,
81-
{
82-
name: prefixName,
83-
},
84-
]);
85-
},
86-
};
48+
<FieldContext.Provider value={{ ...context, prefixName }}>
49+
<Field name={[]} shouldUpdate={shouldUpdate}>
50+
{({ value = [], onChange }) => {
51+
const { getFieldValue } = context;
52+
53+
/**
54+
* Always get latest value in case user update fields by `form` api.
55+
*/
56+
const operations: ListOperations = {
57+
add: () => {
58+
// Mapping keys
59+
keyManager.keys = [...keyManager.keys, keyManager.id];
60+
keyManager.id += 1;
61+
62+
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
63+
onChange([...newValue, undefined]);
64+
},
65+
remove: (index: number) => {
66+
const newValue = (getFieldValue(prefixName) || []) as StoreValue[];
8767

88-
return children(
89-
(value as StoreValue[]).map(
90-
(__, index): ListField => ({
91-
name: index,
92-
key: index,
93-
}),
94-
),
95-
operations,
96-
);
97-
}}
98-
</Field>
99-
</FieldContext.Provider>
100-
);
101-
}}
102-
</FieldContext.Consumer>
68+
// Do not handle out of range
69+
if (index < 0 || index >= newValue.length) {
70+
return;
71+
}
72+
73+
// Update key mapping
74+
const newKeys = keyManager.keys.map((key, id) => {
75+
if (id < index) {
76+
return key;
77+
}
78+
return keyManager.keys[id + 1];
79+
});
80+
keyManager.keys = newKeys.slice(0, -1);
81+
82+
// Trigger store change
83+
onChange(newValue.filter((_, id) => id !== index));
84+
},
85+
};
86+
87+
return children(
88+
(value as StoreValue[]).map(
89+
(__, index): ListField => {
90+
let key = keyManager.keys[index];
91+
if (key === undefined) {
92+
keyManager.keys[index] = keyManager.id;
93+
key = keyManager.keys[index];
94+
keyManager.id += 1;
95+
}
96+
97+
return {
98+
name: index,
99+
key,
100+
};
101+
},
102+
),
103+
operations,
104+
);
105+
}}
106+
</Field>
107+
</FieldContext.Provider>
103108
);
104109
};
105110

src/interface.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export interface FieldEntity {
8686
validateRules: (options?: ValidateOptions) => Promise<string[]>;
8787
getMeta: () => Meta;
8888
getNamePath: () => InternalNamePath;
89+
getErrors: () => string[];
8990
props: {
9091
name?: NamePath;
9192
rules?: Rule[];
@@ -111,7 +112,7 @@ export type ValidateFields = (nameList?: NamePath[]) => Promise<Store>;
111112

112113
export type NotifyInfo =
113114
| {
114-
type: 'valueUpdate' | 'errorUpdate' | 'reset';
115+
type: 'valueUpdate' | 'validateFinish' | 'reset';
115116
source?: 'internal' | 'external';
116117
}
117118
| {

0 commit comments

Comments
 (0)