Skip to content

Commit

Permalink
feat: Support validateOnly (#586)
Browse files Browse the repository at this point in the history
* chore: init ts def

* chore: support validateOnly for only check

* feat: validateOnly support

* test: add test case

* chore: fix ts
  • Loading branch information
zombieJ authored May 11, 2023
1 parent 7a273d0 commit bfdcd2c
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 18 deletions.
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`);
});
});

0 comments on commit bfdcd2c

Please sign in to comment.