Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/demo/validateOnly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## validateOnly

<code src="../examples/validateOnly.tsx" />
76 changes: 76 additions & 0 deletions docs/examples/validateOnly.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<Form form={form}>
<LabelField
name="name"
label="Name"
rules={[
{ required: true },
// { warningOnly: true, validator: () => Promise.reject('Warn Name!') },
]}
>
<Input />
</LabelField>
<LabelField
name="age"
label="Age"
rules={[
{ required: true },
// { warningOnly: true, validator: () => Promise.reject('Warn Age!') },
]}
>
<Input />
</LabelField>
<button type="reset">Reset</button>
<button type="submit" disabled={!canSubmit}>
Submit
</button>
</Form>
<button type="button" onClick={onValidateOnly}>
Validate Without UI update
</button>
</>
);
};
11 changes: 8 additions & 3 deletions src/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
NotifyInfo,
Rule,
Store,
ValidateOptions,
InternalValidateOptions,
InternalFormInstance,
RuleObject,
StoreValue,
Expand Down Expand Up @@ -358,19 +358,20 @@ class Field extends React.Component<InternalFieldProps, FieldState> implements F
}
};

public validateRules = (options?: ValidateOptions): Promise<RuleError[]> => {
public validateRules = (options?: InternalValidateOptions): Promise<RuleError[]> => {
// 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) {
return [];
}

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

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

if (validateOnly) {
return rootPromise;
}

this.validatePromise = rootPromise;
this.dirty = true;
this.errors = EMPTY_ERRORS;
Expand Down
23 changes: 17 additions & 6 deletions src/interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export interface FieldEntity {
isListField: () => boolean;
isList: () => boolean;
isPreserve: () => boolean;
validateRules: (options?: ValidateOptions) => Promise<RuleError[]>;
validateRules: (options?: InternalValidateOptions) => Promise<RuleError[]>;
getMeta: () => Meta;
getNamePath: () => InternalNamePath;
getErrors: () => string[];
Expand All @@ -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<Values = any> = {
(opt?: ValidateOptions): Promise<Values>;
(nameList?: NamePath[], opt?: ValidateOptions): Promise<Values>;
};

export interface InternalValidateOptions extends ValidateOptions {
triggerName?: string;
validateMessages?: ValidateMessages;
/**
Expand All @@ -136,11 +148,10 @@ export interface ValidateOptions {
recursive?: boolean;
}

export type InternalValidateFields<Values = any> = (
nameList?: NamePath[],
options?: ValidateOptions,
) => Promise<Values>;
export type ValidateFields<Values = any> = (nameList?: NamePath[]) => Promise<Values>;
export type InternalValidateFields<Values = any> = {
(options?: InternalValidateOptions): Promise<Values>;
(nameList?: NamePath[], options?: InternalValidateOptions): Promise<Values>;
};

// >>>>>> Info
interface ValueUpdateInfo {
Expand Down
17 changes: 12 additions & 5 deletions src/useForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type {
StoreValue,
ValidateErrorEntity,
ValidateMessages,
ValidateOptions,
InternalValidateOptions,
ValuedNotifyInfo,
WatchCallBack,
} from './interface';
Expand Down Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions src/utils/validateUtil.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as React from 'react';
import warning from 'rc-util/lib/warning';
import type {
InternalNamePath,
ValidateOptions,
InternalValidateOptions,
RuleObject,
StoreValue,
RuleError,
Expand Down Expand Up @@ -31,7 +31,7 @@ async function validateRule(
name: string,
value: StoreValue,
rule: RuleObject,
options: ValidateOptions,
options: InternalValidateOptions,
messageVariables?: Record<string, string>,
): Promise<string[]> {
const cloneRule = { ...rule };
Expand Down Expand Up @@ -123,7 +123,7 @@ export function validateRules(
namePath: InternalNamePath,
value: StoreValue,
rules: RuleObject[],
options: ValidateOptions,
options: InternalValidateOptions,
validateFirst: boolean | 'parallel',
messageVariables?: Record<string, string>,
) {
Expand Down
25 changes: 24 additions & 1 deletion tests/validate.test.tsx
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -867,4 +868,26 @@ describe('Form.Validate', () => {
expect(onMetaChange).toHaveBeenNthCalledWith(3, true);
expect(onMetaChange).toHaveBeenNthCalledWith(4, false);
});

it('validateOnly', async () => {
const formRef = React.createRef<FormInstance>();
const { container } = render(
<Form ref={formRef}>
<InfoField name="test" rules={[{ required: true }]}>
<Input />
</InfoField>
</Form>,
);

// 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`);
});
});