Skip to content

Commit dfc4a0f

Browse files
authored
feat: Validator support promise (#5)
* adjust demo component * support promise * add test case * test context * update prettier config
1 parent d465376 commit dfc4a0f

File tree

13 files changed

+343
-83
lines changed

13 files changed

+343
-83
lines changed

.prettierrc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"singleQuote": true,
3+
"trailingComma": "all",
4+
"printWidth": 100,
5+
"proseWrap": "never"
6+
}

README.md

Lines changed: 59 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@
22

33
React Performance First Form Component.
44

5-
[![NPM version][npm-image]][npm-url]
6-
[![build status][circleci-image]][circleci-url]
7-
[![Test coverage][coveralls-image]][coveralls-url]
8-
[![node version][node-image]][node-url]
9-
[![npm download][download-image]][download-url]
5+
[![NPM version][npm-image]][npm-url] [![build status][circleci-image]][circleci-url] [![Test coverage][coveralls-image]][coveralls-url] [![node version][node-image]][node-url] [![npm download][download-image]][download-url]
106

117
[npm-image]: http://img.shields.io/npm/v/rc-field-form.svg?style=flat-square
128
[npm-url]: http://npmjs.org/package/rc-field-form
@@ -61,43 +57,40 @@ export default Demo;
6157

6258
# API
6359

64-
We use typescript to create the Type definition. You can view directly in IDE.
65-
But you can still check the type definition [here](https://github.com/react-component/field-form/blob/master/src/interface.ts).
60+
We use typescript to create the Type definition. You can view directly in IDE. But you can still check the type definition [here](https://github.com/react-component/field-form/blob/master/src/interface.ts).
6661

6762
## Form
6863

69-
| Prop | Description | Type | Default |
70-
| ---------------- | -------------------------------------------------- | ------------------------------------- | ---------------- |
71-
| fields | Control Form fields status. Only use when in Redux | [FieldData](#fielddata)[] | - |
72-
| form | Set form instance created by `useForm` | [FormInstance](#useform) | `Form.useForm()` |
73-
| initialValues | Initial value of Form | Object | - |
74-
| name | Config name with [FormProvider](#formprovider) | string | - |
75-
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
76-
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields): void | - |
77-
| onValuesChange | Trigger when any value of Field changed | (changedValues, values): void | - |
64+
| Prop | Description | Type | Default |
65+
| --- | --- | --- | --- |
66+
| fields | Control Form fields status. Only use when in Redux | [FieldData](#fielddata)[] | - |
67+
| form | Set form instance created by `useForm` | [FormInstance](#useform) | `Form.useForm()` |
68+
| initialValues | Initial value of Form | Object | - |
69+
| name | Config name with [FormProvider](#formprovider) | string | - |
70+
| validateMessages | Set validate message template | [ValidateMessages](#validatemessages) | - |
71+
| onFieldsChange | Trigger when any value of Field changed | (changedFields, allFields): void | - |
72+
| onValuesChange | Trigger when any value of Field changed | (changedValues, values): void | - |
7873

7974
## Field
8075

81-
| Prop | Description | Type | Default |
82-
| --------------- | --------------------------------------- | --------------------------------- | -------- |
83-
| name | Field name path | [NamePath](#namepath)[] | - |
84-
| rules | Validate rules | [Rule](#rule)[] | - |
85-
| shouldUpdate | Check if Field should update | (prevValues, nextValues): boolean | - |
86-
| trigger | Collect value update by event trigger | string | onChange |
87-
| validateTrigger | Config trigger point with rule validate | string \| string[] | onChange |
76+
| Prop | Description | Type | Default |
77+
| --- | --- | --- | --- |
78+
| name | Field name path | [NamePath](#namepath)[] | - |
79+
| rules | Validate rules | [Rule](#rule)[] | - |
80+
| shouldUpdate | Check if Field should update | (prevValues, nextValues): boolean | - |
81+
| trigger | Collect value update by event trigger | string | onChange |
82+
| validateTrigger | Config trigger point with rule validate | string \| string[] | onChange |
8883

8984
## List
9085

91-
| Prop | Description | Type | Default |
92-
| -------- | ------------------------------- | ----------------------------------------------------------------------------------------------------- | ------- |
93-
| name | List field name path | [NamePath](#namepath)[] | - |
94-
| children | Render props for listing fields | (fields: { name: [NamePath](#namepath) }[], operations: [ListOperations](#listoperations)): ReactNode | - |
86+
| Prop | Description | Type | Default |
87+
| --- | --- | --- | --- |
88+
| name | List field name path | [NamePath](#namepath)[] | - |
89+
| children | Render props for listing fields | (fields: { name: [NamePath](#namepath) }[], operations: [ListOperations](#listoperations)): ReactNode | - |
9590

9691
## useForm
9792

98-
Form component default create an form instance by `Form.useForm`.
99-
But you can create it and pass to Form also.
100-
This allow you to call some function on the form instance.
93+
Form component default create an form instance by `Form.useForm`. But you can create it and pass to Form also. This allow you to call some function on the form instance.
10194

10295
```jsx
10396
const Demo = () => {
@@ -120,26 +113,26 @@ class Demo extends React.Component {
120113
}
121114
```
122115

123-
| Prop | Description | Type |
124-
| ----------------- | ------------------------------------------ | -------------------------------------------------------------------------- |
125-
| getFieldValue | Get field value by name path | (name: [NamePath](#namepath)) => any |
126-
| getFieldsValue | Get list of field values by name path list | (nameList?: [NamePath](#namepath)[]) => any |
127-
| getFieldError | Get field errors by name path | (name: [NamePath](#namepath)) => string[] |
128-
| getFieldsError | Get list of field errors by name path list | (nameList?: [NamePath](#namepath)[]) => FieldError[] |
129-
| isFieldsTouched | Check if list of fields are touched | (nameList?: [NamePath](#namepath)[]) => boolean |
130-
| isFieldTouched | Check if a field is touched | (name: [NamePath](#namepath)) => boolean |
131-
| isFieldValidating | Check if a field is validating | (name: [NamePath](#namepath)) => boolean |
132-
| resetFields | Reset fields status | (fields?: [NamePath](#namepath)[]) => void |
133-
| setFields | Set fields status | (fields: FieldData[]) => void |
134-
| setFieldsValue | Set fields value | (values) => void |
135-
| validateFields | Trigger fields to validate | (nameList?: [NamePath](#namepath)[], options?: ValidateOptions) => Promise |
116+
| Prop | Description | Type |
117+
| --- | --- | --- |
118+
| getFieldValue | Get field value by name path | (name: [NamePath](#namepath)) => any |
119+
| getFieldsValue | Get list of field values by name path list | (nameList?: [NamePath](#namepath)[]) => any |
120+
| getFieldError | Get field errors by name path | (name: [NamePath](#namepath)) => string[] |
121+
| getFieldsError | Get list of field errors by name path list | (nameList?: [NamePath](#namepath)[]) => FieldError[] |
122+
| isFieldsTouched | Check if list of fields are touched | (nameList?: [NamePath](#namepath)[]) => boolean |
123+
| isFieldTouched | Check if a field is touched | (name: [NamePath](#namepath)) => boolean |
124+
| isFieldValidating | Check if a field is validating | (name: [NamePath](#namepath)) => boolean |
125+
| resetFields | Reset fields status | (fields?: [NamePath](#namepath)[]) => void |
126+
| setFields | Set fields status | (fields: FieldData[]) => void |
127+
| setFieldsValue | Set fields value | (values) => void |
128+
| validateFields | Trigger fields to validate | (nameList?: [NamePath](#namepath)[], options?: ValidateOptions) => Promise |
136129

137130
## FormProvider
138131

139-
| Prop | Description | Type | Default |
140-
| ---------------- | ----------------------------------------- | ---------------------------------------- | ------- |
141-
| validateMessages | Config global `validateMessages` template | [ValidateMessages](#validatemessages) | - |
142-
| onFormChange | Trigger by named form fields change | (name, { changedFields, forms }) => void | - |
132+
| Prop | Description | Type | Default |
133+
| --- | --- | --- | --- |
134+
| validateMessages | Config global `validateMessages` template | [ValidateMessages](#validatemessages) | - |
135+
| onFormChange | Trigger by named form fields change | (name, { changedFields, forms }) => void | - |
143136

144137
## Interface
145138

@@ -161,20 +154,24 @@ class Demo extends React.Component {
161154

162155
### Rule
163156

164-
| Prop | Type |
165-
| --------------- | ------------------------------------------------------------------------------------ |
166-
| enum | any[] |
167-
| len | number |
168-
| max | number |
169-
| message | string |
170-
| min | number |
171-
| pattern | RegExp |
172-
| required | boolean |
173-
| transform | (value) => any |
174-
| type | string |
175-
| validator | ([rule](#rule), value, callback: (error?: string) => void, [form](#useform)) => void |
176-
| whitespace | boolean |
177-
| validateTrigger | string \| string[] |
157+
| Prop | Type |
158+
| --- | --- |
159+
| enum | any[] |
160+
| len | number |
161+
| max | number |
162+
| message | string |
163+
| min | number |
164+
| pattern | RegExp |
165+
| required | boolean |
166+
| transform | (value) => any |
167+
| type | string |
168+
| validator | ([rule](#rule), value, callback: (error?: string) => void, [form](#useform)) => Promise \| void |
169+
| whitespace | boolean |
170+
| validateTrigger | string \| string[] |
171+
172+
#### validator
173+
174+
To keep sync with `rc-form` legacy usage of `validator`, we still provides `callback` to trigger validate finished. But in `rc-field-form`, we strongly recommend to return a Promise instead.
178175

179176
### ListOperations
180177

@@ -185,8 +182,7 @@ class Demo extends React.Component {
185182

186183
### ValidateMessages
187184

188-
Validate Messages provides a list of error template.
189-
You can ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) for fully default templates.
185+
Validate Messages provides a list of error template. You can ref [here](https://github.com/react-component/field-form/blob/master/src/utils/messages.ts) for fully default templates.
190186

191187
| Prop | Description |
192188
| ------- | ------------------- |

examples/StateForm-validate-perf.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,11 @@ export default class Demo extends React.Component {
5050
rules={[
5151
{ required: true },
5252
{
53-
validator(_, value, callback, { getFieldValue }) {
53+
async validator(_, value, __, { getFieldValue }) {
5454
if (getFieldValue('password') !== value) {
55-
callback('password2 is not same as password');
56-
return;
55+
return Promise.reject('password2 is not same as password');
5756
}
58-
callback();
57+
return Promise.resolve();
5958
},
6059
},
6160
]}

examples/components/Input.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ const Input = (props: any) => {
44
return <input {...props} />;
55
};
66

7-
const CustomizeInput = (props: any) => (
7+
const CustomizeInput = ({ value = '', ...props }: any) => (
88
<div style={{ padding: 10 }}>
9-
<Input style={{ outline: 'none' }} {...props} />
9+
<Input style={{ outline: 'none' }} value={value} {...props} />
1010
</div>
1111
);
1212

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
"build": "father doc build --storybook",
3232
"compile": "rc-tools run compile --babel-runtime",
3333
"gh-pages": "rc-tools run gh-pages",
34-
"start1": "rc-tools run storybook",
3534
"pub": "rc-tools run pub --babel-runtime",
3635
"lint": "eslint src/**/*",
3736
"test": "father test",
@@ -46,7 +45,7 @@
4645
"enzyme": "^3.1.0",
4746
"enzyme-adapter-react-16": "^1.0.2",
4847
"enzyme-to-json": "^3.1.4",
49-
"father": "^2.6.6",
48+
"father": "^2.7.2",
5049
"react": "^16.8.6",
5150
"react-dom": "^16.8.6",
5251
"react-redux": "^4.4.10",

src/interface.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ export interface Rule {
5252
value: any,
5353
callback: (error?: string) => void,
5454
context: FormInstance, // TODO: Maybe not good place to export this?
55-
) => void;
55+
) => Promise<void> | void;
5656
whitespace?: boolean;
5757

5858
/** Customize rule level `validateTrigger`. Must be subset of Field `validateTrigger` */

src/useForm.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as React from 'react';
2+
import warning from 'warning';
23
import {
34
Callbacks,
45
FieldData,
@@ -89,7 +90,7 @@ export class FormStore {
8990
};
9091
}
9192

92-
console.error('`getInternalHooks` is internal usage. Should not call directly.');
93+
warning(false, '`getInternalHooks` is internal usage. Should not call directly.');
9394
return null;
9495
};
9596

src/utils/validateUtil.ts

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import AsyncValidator from 'async-validator';
2+
import warning from 'warning';
23
import {
34
FieldError,
45
InternalNamePath,
@@ -61,7 +62,7 @@ function convertMessages(messages: ValidateMessages, name: string, rule: Rule) {
6162
return fillTemplate(setValues({}, defaultValidateMessages, messages));
6263
}
6364

64-
function validateRule(
65+
async function validateRule(
6566
name: string,
6667
value: any,
6768
rule: Rule,
@@ -74,14 +75,15 @@ function validateRule(
7475
const messages = convertMessages(options.validateMessages, name, rule);
7576
validator.messages(messages);
7677

77-
return Promise.resolve(validator.validate({ [name]: value }, { ...options }))
78-
.then(() => [])
79-
.catch(errObj => {
80-
if (errObj.errors) {
81-
return errObj.errors.map(e => e.message);
82-
}
83-
return messages.default();
84-
});
78+
try {
79+
await Promise.resolve(validator.validate({ [name]: value }, { ...options }));
80+
return [];
81+
} catch (errObj) {
82+
if (errObj.errors) {
83+
return errObj.errors.map(e => e.message);
84+
}
85+
return messages.default();
86+
}
8587
}
8688

8789
/**
@@ -105,7 +107,43 @@ export function validateRules(
105107
return {
106108
...currentRule,
107109
validator(rule: any, val: any, callback: any) {
108-
currentRule.validator(rule, val, callback, context);
110+
let hasPromise = false;
111+
112+
// Wrap callback only accept when promise not provided
113+
const wrappedCallback = (...args: string[]) => {
114+
// Wait a tick to make sure return type is a promise
115+
Promise.resolve().then(() => {
116+
warning(
117+
!hasPromise,
118+
'Your validator function has already return a promise. `callback` will be ignored.',
119+
);
120+
121+
if (!hasPromise) {
122+
callback(...args);
123+
}
124+
});
125+
};
126+
127+
// Get promise
128+
const promise = currentRule.validator(rule, val, wrappedCallback, context);
129+
hasPromise =
130+
promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
131+
132+
/**
133+
* 1. Use promise as the first priority.
134+
* 2. If promise not exist, use callback with warning instead
135+
*/
136+
warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
137+
138+
if (hasPromise) {
139+
(promise as Promise<void>)
140+
.then(() => {
141+
callback();
142+
})
143+
.catch(err => {
144+
callback(err);
145+
});
146+
}
109147
},
110148
};
111149
});

tests/common/InfoField.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React, { ReactElement } from 'react';
2+
import { Field } from '../../src';
3+
import { FieldProps } from '../../src/Field';
4+
5+
interface InfoFieldProps extends FieldProps {
6+
children: ReactElement;
7+
}
8+
9+
/**
10+
* Return a wrapped Field with meta info
11+
*/
12+
const InfoField: React.FC<InfoFieldProps> = ({ children, ...props }) => (
13+
<Field {...props}>
14+
{(control, { errors }) => (
15+
<div>
16+
{children ? (
17+
React.cloneElement(children, control)
18+
) : (
19+
<input {...control} value={control.value || ''} />
20+
)}
21+
<ul className="errors">
22+
{errors.map(error => (
23+
<li key={error}>{error}</li>
24+
))}
25+
</ul>
26+
</div>
27+
)}
28+
</Field>
29+
);
30+
31+
export default InfoField;

tests/common/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import timeout from './timeout';
2+
3+
export async function changeValue(wrapper, value) {
4+
wrapper.find('input').simulate('change', { target: { value } });
5+
await timeout();
6+
wrapper.update();
7+
}
8+
9+
export function matchError(wrapper, error) {
10+
if (error) {
11+
expect(wrapper.find('.errors li').length).toBeTruthy();
12+
} else {
13+
expect(wrapper.find('.errors li').length).toBeFalsy();
14+
}
15+
16+
if (error && typeof error !== 'boolean') {
17+
expect(wrapper.find('.errors li').text()).toBe(error);
18+
}
19+
}

0 commit comments

Comments
 (0)