Skip to content

Commit bfdcd2c

Browse files
authored
feat: Support validateOnly (#586)
* chore: init ts def * chore: support validateOnly for only check * feat: validateOnly support * test: add test case * chore: fix ts
1 parent 7a273d0 commit bfdcd2c

File tree

7 files changed

+143
-18
lines changed

7 files changed

+143
-18
lines changed

docs/demo/validateOnly.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## validateOnly
2+
3+
<code src="../examples/validateOnly.tsx" />

docs/examples/validateOnly.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/* eslint-disable react/prop-types, @typescript-eslint/consistent-type-imports */
2+
3+
import React from 'react';
4+
import Form from 'rc-field-form';
5+
import type { FormInstance } from 'rc-field-form';
6+
import Input from './components/Input';
7+
import LabelField from './components/LabelField';
8+
9+
function useSubmittable(form: FormInstance) {
10+
const [submittable, setSubmittable] = React.useState(false);
11+
const store = Form.useWatch([], form);
12+
13+
React.useEffect(() => {
14+
form
15+
.validateFields({
16+
validateOnly: true,
17+
})
18+
.then(
19+
() => {
20+
setSubmittable(true);
21+
},
22+
() => {
23+
setSubmittable(false);
24+
},
25+
);
26+
}, [store]);
27+
28+
return submittable;
29+
}
30+
31+
export default () => {
32+
const [form] = Form.useForm();
33+
34+
const canSubmit = useSubmittable(form);
35+
36+
const onValidateOnly = async () => {
37+
const result = await form.validateFields({
38+
validateOnly: true,
39+
});
40+
console.log('Validate:', result);
41+
};
42+
43+
return (
44+
<>
45+
<Form form={form}>
46+
<LabelField
47+
name="name"
48+
label="Name"
49+
rules={[
50+
{ required: true },
51+
// { warningOnly: true, validator: () => Promise.reject('Warn Name!') },
52+
]}
53+
>
54+
<Input />
55+
</LabelField>
56+
<LabelField
57+
name="age"
58+
label="Age"
59+
rules={[
60+
{ required: true },
61+
// { warningOnly: true, validator: () => Promise.reject('Warn Age!') },
62+
]}
63+
>
64+
<Input />
65+
</LabelField>
66+
<button type="reset">Reset</button>
67+
<button type="submit" disabled={!canSubmit}>
68+
Submit
69+
</button>
70+
</Form>
71+
<button type="button" onClick={onValidateOnly}>
72+
Validate Without UI update
73+
</button>
74+
</>
75+
);
76+
};

src/Field.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
NotifyInfo,
1111
Rule,
1212
Store,
13-
ValidateOptions,
13+
InternalValidateOptions,
1414
InternalFormInstance,
1515
RuleObject,
1616
StoreValue,
@@ -358,19 +358,20 @@ class Field extends React.Component<InternalFieldProps, FieldState> implements F
358358
}
359359
};
360360

361-
public validateRules = (options?: ValidateOptions): Promise<RuleError[]> => {
361+
public validateRules = (options?: InternalValidateOptions): Promise<RuleError[]> => {
362362
// We should fixed namePath & value to avoid developer change then by form function
363363
const namePath = this.getNamePath();
364364
const currentValue = this.getValue();
365365

366+
const { triggerName, validateOnly = false } = options || {};
367+
366368
// Force change to async to avoid rule OOD under renderProps field
367369
const rootPromise = Promise.resolve().then(() => {
368370
if (!this.mounted) {
369371
return [];
370372
}
371373

372374
const { validateFirst = false, messageVariables } = this.props;
373-
const { triggerName } = (options || {}) as ValidateOptions;
374375

375376
let filteredRules = this.getRules();
376377
if (triggerName) {
@@ -423,6 +424,10 @@ class Field extends React.Component<InternalFieldProps, FieldState> implements F
423424
return promise;
424425
});
425426

427+
if (validateOnly) {
428+
return rootPromise;
429+
}
430+
426431
this.validatePromise = rootPromise;
427432
this.dirty = true;
428433
this.errors = EMPTY_ERRORS;

src/interface.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ export interface FieldEntity {
102102
isListField: () => boolean;
103103
isList: () => boolean;
104104
isPreserve: () => boolean;
105-
validateRules: (options?: ValidateOptions) => Promise<RuleError[]>;
105+
validateRules: (options?: InternalValidateOptions) => Promise<RuleError[]>;
106106
getMeta: () => Meta;
107107
getNamePath: () => InternalNamePath;
108108
getErrors: () => string[];
@@ -127,6 +127,18 @@ export interface RuleError {
127127
}
128128

129129
export interface ValidateOptions {
130+
/**
131+
* Validate only and not trigger UI and Field status update
132+
*/
133+
validateOnly?: boolean;
134+
}
135+
136+
export type ValidateFields<Values = any> = {
137+
(opt?: ValidateOptions): Promise<Values>;
138+
(nameList?: NamePath[], opt?: ValidateOptions): Promise<Values>;
139+
};
140+
141+
export interface InternalValidateOptions extends ValidateOptions {
130142
triggerName?: string;
131143
validateMessages?: ValidateMessages;
132144
/**
@@ -136,11 +148,10 @@ export interface ValidateOptions {
136148
recursive?: boolean;
137149
}
138150

139-
export type InternalValidateFields<Values = any> = (
140-
nameList?: NamePath[],
141-
options?: ValidateOptions,
142-
) => Promise<Values>;
143-
export type ValidateFields<Values = any> = (nameList?: NamePath[]) => Promise<Values>;
151+
export type InternalValidateFields<Values = any> = {
152+
(options?: InternalValidateOptions): Promise<Values>;
153+
(nameList?: NamePath[], options?: InternalValidateOptions): Promise<Values>;
154+
};
144155

145156
// >>>>>> Info
146157
interface ValueUpdateInfo {

src/useForm.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import type {
2020
StoreValue,
2121
ValidateErrorEntity,
2222
ValidateMessages,
23-
ValidateOptions,
23+
InternalValidateOptions,
2424
ValuedNotifyInfo,
2525
WatchCallBack,
2626
} from './interface';
@@ -836,12 +836,19 @@ export class FormStore {
836836
};
837837

838838
// =========================== Validate ===========================
839-
private validateFields: InternalValidateFields = (
840-
nameList?: NamePath[],
841-
options?: ValidateOptions,
842-
) => {
839+
private validateFields: InternalValidateFields = (arg1?: any, arg2?: any) => {
843840
this.warningUnhooked();
844841

842+
let nameList: NamePath[];
843+
let options: InternalValidateOptions;
844+
845+
if (Array.isArray(arg1) || typeof arg1 === 'string' || typeof arg2 === 'string') {
846+
nameList = arg1;
847+
options = arg2;
848+
} else {
849+
options = arg1;
850+
}
851+
845852
const provideNameList = !!nameList;
846853
const namePathList: InternalNamePath[] | undefined = provideNameList
847854
? nameList.map(getNamePath)

src/utils/validateUtil.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as React from 'react';
33
import warning from 'rc-util/lib/warning';
44
import type {
55
InternalNamePath,
6-
ValidateOptions,
6+
InternalValidateOptions,
77
RuleObject,
88
StoreValue,
99
RuleError,
@@ -31,7 +31,7 @@ async function validateRule(
3131
name: string,
3232
value: StoreValue,
3333
rule: RuleObject,
34-
options: ValidateOptions,
34+
options: InternalValidateOptions,
3535
messageVariables?: Record<string, string>,
3636
): Promise<string[]> {
3737
const cloneRule = { ...rule };
@@ -123,7 +123,7 @@ export function validateRules(
123123
namePath: InternalNamePath,
124124
value: StoreValue,
125125
rules: RuleObject[],
126-
options: ValidateOptions,
126+
options: InternalValidateOptions,
127127
validateFirst: boolean | 'parallel',
128128
messageVariables?: Record<string, string>,
129129
) {

tests/validate.test.tsx

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import React, { useEffect } from 'react';
2+
import { render } from '@testing-library/react';
23
import { mount } from 'enzyme';
34
import { act } from 'react-dom/test-utils';
45
import Form, { Field, useForm } from '../src';
56
import InfoField, { Input } from './common/InfoField';
67
import { changeValue, matchError, getField } from './common';
78
import timeout from './common/timeout';
8-
import type { ValidateMessages } from '@/interface';
9+
import type { FormInstance, ValidateMessages } from '../src/interface';
910

1011
describe('Form.Validate', () => {
1112
it('required', async () => {
@@ -867,4 +868,26 @@ describe('Form.Validate', () => {
867868
expect(onMetaChange).toHaveBeenNthCalledWith(3, true);
868869
expect(onMetaChange).toHaveBeenNthCalledWith(4, false);
869870
});
871+
872+
it('validateOnly', async () => {
873+
const formRef = React.createRef<FormInstance>();
874+
const { container } = render(
875+
<Form ref={formRef}>
876+
<InfoField name="test" rules={[{ required: true }]}>
877+
<Input />
878+
</InfoField>
879+
</Form>,
880+
);
881+
882+
// Validate only
883+
const result = await formRef.current.validateFields({ validateOnly: true }).catch(e => e);
884+
await timeout();
885+
expect(result.errorFields).toHaveLength(1);
886+
expect(container.querySelector('.errors').textContent).toBeFalsy();
887+
888+
// Normal validate
889+
await formRef.current.validateFields().catch(e => e);
890+
await timeout();
891+
expect(container.querySelector('.errors').textContent).toEqual(`'test' is required`);
892+
});
870893
});

0 commit comments

Comments
 (0)