diff --git a/docs/demo/validateOnly.md b/docs/demo/validateOnly.md new file mode 100644 index 00000000..2be92f39 --- /dev/null +++ b/docs/demo/validateOnly.md @@ -0,0 +1,3 @@ +## validateOnly + + diff --git a/docs/examples/validateOnly.tsx b/docs/examples/validateOnly.tsx new file mode 100644 index 00000000..df561830 --- /dev/null +++ b/docs/examples/validateOnly.tsx @@ -0,0 +1,76 @@ +/* eslint-disable react/prop-types, @typescript-eslint/consistent-type-imports */ + +import React from 'react'; +import Form from 'rc-field-form'; +import type { FormInstance } from 'rc-field-form'; +import Input from './components/Input'; +import LabelField from './components/LabelField'; + +function useSubmittable(form: FormInstance) { + const [submittable, setSubmittable] = React.useState(false); + const store = Form.useWatch([], form); + + React.useEffect(() => { + form + .validateFields({ + validateOnly: true, + }) + .then( + () => { + setSubmittable(true); + }, + () => { + setSubmittable(false); + }, + ); + }, [store]); + + return submittable; +} + +export default () => { + const [form] = Form.useForm(); + + const canSubmit = useSubmittable(form); + + const onValidateOnly = async () => { + const result = await form.validateFields({ + validateOnly: true, + }); + console.log('Validate:', result); + }; + + return ( + <> +
+ Promise.reject('Warn Name!') }, + ]} + > + + + Promise.reject('Warn Age!') }, + ]} + > + + + + +
+ + + ); +}; diff --git a/src/Field.tsx b/src/Field.tsx index 806708fb..ce706ff9 100644 --- a/src/Field.tsx +++ b/src/Field.tsx @@ -10,7 +10,7 @@ import type { NotifyInfo, Rule, Store, - ValidateOptions, + InternalValidateOptions, InternalFormInstance, RuleObject, StoreValue, @@ -358,11 +358,13 @@ class Field extends React.Component implements F } }; - public validateRules = (options?: ValidateOptions): Promise => { + public validateRules = (options?: InternalValidateOptions): Promise => { // We should fixed namePath & value to avoid developer change then by form function const namePath = this.getNamePath(); const currentValue = this.getValue(); + const { triggerName, validateOnly = false } = options || {}; + // Force change to async to avoid rule OOD under renderProps field const rootPromise = Promise.resolve().then(() => { if (!this.mounted) { @@ -370,7 +372,6 @@ class Field extends React.Component implements F } const { validateFirst = false, messageVariables } = this.props; - const { triggerName } = (options || {}) as ValidateOptions; let filteredRules = this.getRules(); if (triggerName) { @@ -423,6 +424,10 @@ class Field extends React.Component implements F return promise; }); + if (validateOnly) { + return rootPromise; + } + this.validatePromise = rootPromise; this.dirty = true; this.errors = EMPTY_ERRORS; diff --git a/src/interface.ts b/src/interface.ts index 3413d602..2fd74a4c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -102,7 +102,7 @@ export interface FieldEntity { isListField: () => boolean; isList: () => boolean; isPreserve: () => boolean; - validateRules: (options?: ValidateOptions) => Promise; + validateRules: (options?: InternalValidateOptions) => Promise; getMeta: () => Meta; getNamePath: () => InternalNamePath; getErrors: () => string[]; @@ -127,6 +127,18 @@ export interface RuleError { } export interface ValidateOptions { + /** + * Validate only and not trigger UI and Field status update + */ + validateOnly?: boolean; +} + +export type ValidateFields = { + (opt?: ValidateOptions): Promise; + (nameList?: NamePath[], opt?: ValidateOptions): Promise; +}; + +export interface InternalValidateOptions extends ValidateOptions { triggerName?: string; validateMessages?: ValidateMessages; /** @@ -136,11 +148,10 @@ export interface ValidateOptions { recursive?: boolean; } -export type InternalValidateFields = ( - nameList?: NamePath[], - options?: ValidateOptions, -) => Promise; -export type ValidateFields = (nameList?: NamePath[]) => Promise; +export type InternalValidateFields = { + (options?: InternalValidateOptions): Promise; + (nameList?: NamePath[], options?: InternalValidateOptions): Promise; +}; // >>>>>> Info interface ValueUpdateInfo { diff --git a/src/useForm.ts b/src/useForm.ts index 1d497e85..f0af0bad 100644 --- a/src/useForm.ts +++ b/src/useForm.ts @@ -20,7 +20,7 @@ import type { StoreValue, ValidateErrorEntity, ValidateMessages, - ValidateOptions, + InternalValidateOptions, ValuedNotifyInfo, WatchCallBack, } from './interface'; @@ -836,12 +836,19 @@ export class FormStore { }; // =========================== Validate =========================== - private validateFields: InternalValidateFields = ( - nameList?: NamePath[], - options?: ValidateOptions, - ) => { + private validateFields: InternalValidateFields = (arg1?: any, arg2?: any) => { this.warningUnhooked(); + let nameList: NamePath[]; + let options: InternalValidateOptions; + + if (Array.isArray(arg1) || typeof arg1 === 'string' || typeof arg2 === 'string') { + nameList = arg1; + options = arg2; + } else { + options = arg1; + } + const provideNameList = !!nameList; const namePathList: InternalNamePath[] | undefined = provideNameList ? nameList.map(getNamePath) diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts index 7270ac1e..dcff5ea3 100644 --- a/src/utils/validateUtil.ts +++ b/src/utils/validateUtil.ts @@ -3,7 +3,7 @@ import * as React from 'react'; import warning from 'rc-util/lib/warning'; import type { InternalNamePath, - ValidateOptions, + InternalValidateOptions, RuleObject, StoreValue, RuleError, @@ -31,7 +31,7 @@ async function validateRule( name: string, value: StoreValue, rule: RuleObject, - options: ValidateOptions, + options: InternalValidateOptions, messageVariables?: Record, ): Promise { const cloneRule = { ...rule }; @@ -123,7 +123,7 @@ export function validateRules( namePath: InternalNamePath, value: StoreValue, rules: RuleObject[], - options: ValidateOptions, + options: InternalValidateOptions, validateFirst: boolean | 'parallel', messageVariables?: Record, ) { diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx index ef4a5e40..7d36aca8 100644 --- a/tests/validate.test.tsx +++ b/tests/validate.test.tsx @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; +import { render } from '@testing-library/react'; import { mount } from 'enzyme'; import { act } from 'react-dom/test-utils'; import Form, { Field, useForm } from '../src'; import InfoField, { Input } from './common/InfoField'; import { changeValue, matchError, getField } from './common'; import timeout from './common/timeout'; -import type { ValidateMessages } from '@/interface'; +import type { FormInstance, ValidateMessages } from '../src/interface'; describe('Form.Validate', () => { it('required', async () => { @@ -867,4 +868,26 @@ describe('Form.Validate', () => { expect(onMetaChange).toHaveBeenNthCalledWith(3, true); expect(onMetaChange).toHaveBeenNthCalledWith(4, false); }); + + it('validateOnly', async () => { + const formRef = React.createRef(); + const { container } = render( +
+ + + +
, + ); + + // Validate only + const result = await formRef.current.validateFields({ validateOnly: true }).catch(e => e); + await timeout(); + expect(result.errorFields).toHaveLength(1); + expect(container.querySelector('.errors').textContent).toBeFalsy(); + + // Normal validate + await formRef.current.validateFields().catch(e => e); + await timeout(); + expect(container.querySelector('.errors').textContent).toEqual(`'test' is required`); + }); });