Skip to content

Commit b4a714e

Browse files
authored
chore: Add legacy test [part 1] (#8)
* add legacy test * sync logic with legacy validate * update test case & doc * support rule be a validator function directly * throw if out of date * add clean-field test * fix prettier * mainly test * add dynamic-binding test
1 parent 5660128 commit b4a714e

15 files changed

+713
-32
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const base = require('father/template/.eslintrc.js');
33
base.rules['no-template-curly-in-string'] = 0;
44
base.rules['promise/always-return'] = 0;
55
base.rules['promise/catch-or-return'] = 0;
6+
base.rules['promise/no-callback-in-promise'] = 0;
67
base.rules['prefer-promise-reject-errors'] = 0;
78

89
module.exports = base;

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,3 +194,58 @@ Validate Messages provides a list of error template. You can ref [here](https://
194194
| name | Field name |
195195
| pattern | Rule `pattern` prop |
196196
| type | Rule `type` prop |
197+
198+
# Different with `rc-form`
199+
200+
`rc-field-form` is try to keep sync with `rc-form` in api level, but there still have something to change:
201+
202+
## 🔥 Remove Field will not clean up related value
203+
204+
We do lots of logic to clean up the value when Field removed before. But with user feedback, remove exist value increase the additional work to keep value back with conditional field.
205+
206+
## 🔥 Nest name use array instead of string
207+
208+
In `rc-form`, we support like `user.name` to be a name and convert value to `{ user: { name: 'Bamboo' } }`. This makes '.' always be the route of variable, this makes developer have to do additional work if name is real contains a point like `app.config.start` to be `app_config_start` and parse back to point when submit.
209+
210+
Field Form will only trade `['user', 'name']` to be `{ user: { name: 'Bamboo' } }`, and `user.name` to be `{ ['user.name']: 'Bamboo' }`.
211+
212+
## 🔥 `getFieldsError` always return array
213+
214+
`rc-form` returns `null` when no error happen. This makes user have to do some additional code like:
215+
216+
```js
217+
(form.getFieldsError('fieldName') || []).forEach(() => {
218+
// Do something...
219+
});
220+
```
221+
222+
Now `getFieldsError` will return `[]` if no errors.
223+
224+
## 🔥 Remove `callback` with `validateFields`
225+
226+
Since ES8 is support `async/await`, that's no reason not to use it. Now you can easily handle your validate logic:
227+
228+
```js
229+
async function() {
230+
try {
231+
const values = await form.validateFields();
232+
console.log(values);
233+
} catch (errorList) {
234+
errorList.forEach(({ name, errors }) => {
235+
// Do something...
236+
});
237+
}
238+
}
239+
```
240+
241+
**Notice: Now if your validator return an `Error(message)`, not need to get error by `e => e.message`. FieldForm will handle this.**
242+
243+
## 🔥 `preserve` is no need anymore
244+
245+
In `rc-form` you should use `preserve` to keep a value cause Form will auto remove a value from Field removed. Field Form will always keep the value in the Form whatever Field removed.
246+
247+
## 🔥 `setFields` not trigger `onFieldsChange` and `setFieldsValue` not trigger `onValuesChange`
248+
249+
In `rc-form`, we hope to help user auto trigger change event by setting to make redux dispatch easier, but it's not good design since it makes code logic couping.
250+
251+
Additionally, user control update trigger `onFieldsChange` & `onValuesChange` event has potential dead loop risk.

src/Field.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,12 @@ class Field extends React.Component<FieldProps, FieldState> implements FieldEnti
214214

215215
let filteredRules = rules || [];
216216
if (triggerName) {
217-
filteredRules = filteredRules.filter(({ validateTrigger }: Rule) => {
217+
filteredRules = filteredRules.filter((rule: Rule) => {
218+
if (typeof rule === 'function') {
219+
return true;
220+
}
221+
222+
const { validateTrigger } = rule;
218223
if (!validateTrigger) {
219224
return true;
220225
}

src/FieldContext.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const Context = React.createContext<InternalFormInstance>({
1515
isFieldsTouched: warningFunc,
1616
isFieldTouched: warningFunc,
1717
isFieldValidating: warningFunc,
18+
isFieldsValidating: warningFunc,
1819
resetFields: warningFunc,
1920
setFields: warningFunc,
2021
setFieldsValue: warningFunc,

src/interface.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,14 @@ export type RuleType =
3737
| 'hex'
3838
| 'email';
3939

40-
export interface Rule {
40+
type Validator = (
41+
rule: Rule,
42+
value: any,
43+
callback: (error?: string) => void,
44+
context: FormInstance, // TODO: Maybe not good place to export this?
45+
) => Promise<void> | void;
46+
47+
export interface RuleObject {
4148
enum?: any[];
4249
len?: number;
4350
max?: number;
@@ -47,18 +54,15 @@ export interface Rule {
4754
required?: boolean;
4855
transform?: (value: any) => any;
4956
type?: RuleType;
50-
validator?: (
51-
rule: Rule,
52-
value: any,
53-
callback: (error?: string) => void,
54-
context: FormInstance, // TODO: Maybe not good place to export this?
55-
) => Promise<void> | void;
57+
validator?: Validator;
5658
whitespace?: boolean;
5759

5860
/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */
5961
validateTrigger?: string | string[];
6062
}
6163

64+
export type Rule = RuleObject | Validator;
65+
6266
export interface FieldEntity {
6367
onStoreChange: (store: any, namePathList: InternalNamePath[] | null, info: NotifyInfo) => void;
6468
isFieldTouched: () => boolean;
@@ -132,6 +136,7 @@ export interface FormInstance {
132136
isFieldsTouched: (nameList?: NamePath[]) => boolean;
133137
isFieldTouched: (name: NamePath) => boolean;
134138
isFieldValidating: (name: NamePath) => boolean;
139+
isFieldsValidating: (nameList: NamePath[]) => boolean;
135140
resetFields: (fields?: NamePath[]) => void;
136141
setFields: (fields: FieldData[]) => void;
137142
setFieldsValue: (value: Store) => void;

src/useForm.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import {
2626
containsNamePath,
2727
getNamePath,
2828
getValue,
29-
matchNamePath,
3029
setValue,
3130
setValues,
3231
} from './utils/valueUtil';
@@ -56,6 +55,8 @@ export class FormStore {
5655

5756
private validateMessages: ValidateMessages = null;
5857

58+
private lastValidatePromise: Promise<FieldError[]> = null;
59+
5960
constructor(forceRootUpdate: () => void) {
6061
this.forceRootUpdate = forceRootUpdate;
6162
}
@@ -68,10 +69,12 @@ export class FormStore {
6869
isFieldsTouched: this.isFieldsTouched,
6970
isFieldTouched: this.isFieldTouched,
7071
isFieldValidating: this.isFieldValidating,
72+
isFieldsValidating: this.isFieldsValidating,
7173
resetFields: this.resetFields,
7274
setFields: this.setFields,
7375
setFieldsValue: this.setFieldsValue,
7476
validateFields: this.validateFields,
77+
// TODO: validateFieldsAndScroll
7578

7679
getInternalHooks: this.getInternalHooks,
7780
});
@@ -185,14 +188,21 @@ export class FormStore {
185188
return this.isFieldsTouched([name]);
186189
};
187190

188-
private isFieldValidating = (name: NamePath) => {
189-
const namePath: InternalNamePath = getNamePath(name);
190-
const field = this.getFieldEntities().find(testField => {
191+
private isFieldsValidating = (nameList?: NamePath[]) => {
192+
const fieldEntities = this.getFieldEntities();
193+
if (!nameList) {
194+
return fieldEntities.some(testField => testField.isFieldValidating());
195+
}
196+
197+
const namePathList: InternalNamePath[] = nameList.map(getNamePath);
198+
return fieldEntities.some(testField => {
191199
const fieldNamePath = testField.getNamePath();
192-
return matchNamePath(fieldNamePath, namePath);
200+
return containsNamePath(namePathList, fieldNamePath) && testField.isFieldValidating();
193201
});
202+
};
194203

195-
return field && field.isFieldValidating();
204+
private isFieldValidating = (name: NamePath) => {
205+
return this.isFieldsValidating([name]);
196206
};
197207

198208
private resetFields = (nameList?: NamePath[]) => {
@@ -397,12 +407,25 @@ export class FormStore {
397407
};
398408

399409
// =========================== Validate ===========================
410+
// TODO: Cache validate result to avoid duplicated validate???
400411
private validateFields: InternalValidateFields = (
401412
nameList?: NamePath[],
402413
options?: ValidateOptions,
403414
) => {
404415
const namePathList: InternalNamePath[] | undefined = nameList && nameList.map(getNamePath);
405416

417+
// Clean up origin errors
418+
if (namePathList) {
419+
this.errorCache.updateError(
420+
namePathList.map(name => ({
421+
name,
422+
errors: [],
423+
})),
424+
);
425+
} else {
426+
this.errorCache = new ErrorCache();
427+
}
428+
406429
// Collect result in promise list
407430
const promiseList: Promise<any>[] = [];
408431

@@ -437,6 +460,7 @@ export class FormStore {
437460
});
438461

439462
const summaryPromise = allPromiseFinish(promiseList);
463+
this.lastValidatePromise = summaryPromise;
440464

441465
// Notify fields with rule that validate has finished and need update
442466
summaryPromise
@@ -450,10 +474,19 @@ export class FormStore {
450474
});
451475

452476
const returnPromise = summaryPromise
453-
.then(() => this.store)
454-
.catch((results: any) => {
455-
const errorList = results.filter((result: any) => result);
456-
return Promise.reject(errorList);
477+
.then(() => {
478+
if (this.lastValidatePromise === summaryPromise) {
479+
return this.store;
480+
}
481+
return Promise.reject([]);
482+
})
483+
.catch((results: { name: InternalNamePath; errors: string[] }[]) => {
484+
const errorList = results.filter(result => result);
485+
return Promise.reject({
486+
values: this.store,
487+
errorFields: errorList,
488+
outOfDate: this.lastValidatePromise !== summaryPromise,
489+
});
457490
});
458491

459492
// Do not throw in console

src/utils/asyncUtil.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
export function allPromiseFinish(promiseList: Promise<any>[]) {
1+
import { FieldError } from '../interface';
2+
3+
export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
24
let hasError = false;
35
let count = promiseList.length;
46
const results: any[] = [];
57

8+
if (!promiseList.length) {
9+
return Promise.resolve([]);
10+
}
11+
612
return new Promise((resolve, reject) => {
713
promiseList.forEach((promise, index) => {
814
promise

src/utils/validateUtil.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,16 @@ function replaceMessage(template: string, kv: { [name: string]: any }): string {
2828
* { required: '${name} is required' } => { required: () => 'field is required' }
2929
*/
3030
function convertMessages(messages: ValidateMessages, name: string, rule: Rule) {
31-
const kv: { [name: string]: any } = {
32-
...rule,
33-
name,
34-
enum: (rule.enum || []).join(', '),
35-
};
31+
const kv: { [name: string]: any } =
32+
typeof rule === 'function'
33+
? {
34+
name,
35+
}
36+
: {
37+
...rule,
38+
name,
39+
enum: (rule.enum || []).join(', '),
40+
};
3641

3742
const replaceFunc = (template: string, additionalKV?: Record<string, any>) => {
3843
if (!template) return null;
@@ -101,7 +106,10 @@ export function validateRules(
101106

102107
// Fill rule with context
103108
const filledRules: Rule[] = rules.map(currentRule => {
104-
if (!currentRule.validator) {
109+
const originValidatorFunc =
110+
typeof currentRule === 'function' ? currentRule : currentRule.validator;
111+
112+
if (!originValidatorFunc) {
105113
return currentRule;
106114
}
107115
return {
@@ -125,7 +133,7 @@ export function validateRules(
125133
};
126134

127135
// Get promise
128-
const promise = currentRule.validator(rule, val, wrappedCallback, context);
136+
const promise = originValidatorFunc(rule, val, wrappedCallback, context);
129137
hasPromise =
130138
promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
131139

tests/common/index.js

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import timeout from './timeout';
2-
import InfoField, { Input } from './InfoField';
2+
import { Field } from '../../src';
3+
import { getNamePath, matchNamePath } from '../../src/utils/valueUtil';
34

45
export async function changeValue(wrapper, value) {
56
wrapper.find('input').simulate('change', { target: { value } });
@@ -20,9 +21,42 @@ export function matchError(wrapper, error) {
2021
}
2122

2223
export function getField(wrapper, index = 0) {
23-
return wrapper.find(InfoField).at(index);
24+
if (typeof index === 'number') {
25+
return wrapper.find(Field).at(index);
26+
}
27+
28+
const name = getNamePath(index);
29+
const fields = wrapper.find(Field);
30+
for (let i = 0; i < fields.length; i += 1) {
31+
const field = fields.at(i);
32+
const fieldName = getNamePath(field.props().name);
33+
34+
if (matchNamePath(name, fieldName)) {
35+
return field;
36+
}
37+
}
38+
return null;
2439
}
2540

26-
export function getInput(wrapper, index = 0) {
27-
return wrapper.find(Input).at(index);
41+
export function matchArray(source, target, matchKey) {
42+
expect(matchKey).toBeTruthy();
43+
44+
try {
45+
expect(source.length).toBe(target.length);
46+
} catch (err) {
47+
throw new Error(
48+
`
49+
Array length not match.
50+
source(${source.length}): ${JSON.stringify(source)}
51+
target(${target.length}): ${JSON.stringify(target)}
52+
`.trim(),
53+
);
54+
}
55+
56+
target.forEach(tgt => {
57+
const matchValue = tgt[matchKey];
58+
const src = source.find(item => matchNamePath(item[matchKey], matchValue));
59+
expect(src).toBeTruthy();
60+
expect(src).toMatchObject(tgt);
61+
});
2862
}

tests/dependencies.test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React from 'react';
22
import { mount } from 'enzyme';
33
import Form, { Field } from '../src';
44
import InfoField, { Input } from './common/InfoField';
5-
import { changeValue, matchError, getField, getInput } from './common';
5+
import { changeValue, matchError, getField } from './common';
66

77
describe('dependencies', () => {
88
it('touched', async () => {
@@ -65,7 +65,7 @@ describe('dependencies', () => {
6565
]);
6666

6767
rendered = false;
68-
await changeValue(getInput(wrapper), '1');
68+
await changeValue(getField(wrapper), '1');
6969

7070
expect(rendered).toBeTruthy();
7171
});

0 commit comments

Comments
 (0)