diff --git a/docs/examples/components/LabelField.tsx b/docs/examples/components/LabelField.tsx
index 2790fa9e..2608f2bd 100644
--- a/docs/examples/components/LabelField.tsx
+++ b/docs/examples/components/LabelField.tsx
@@ -1,11 +1,16 @@
import * as React from 'react';
import Form from 'rc-field-form';
-import { FieldProps } from '@/Field';
+import type { FieldProps } from '@/Field';
const { Field } = Form;
-const Error = ({ children }) => (
-
+interface ErrorProps {
+ warning?: boolean;
+ children?: React.ReactNode[];
+}
+
+const Error = ({ children, warning }: ErrorProps) => (
+
{children.map((error: React.ReactNode, index: number) => (
- {error}
))}
@@ -55,6 +60,7 @@ const LabelField: React.FunctionComponent = ({
{meta.errors}
+ {meta.warnings}
);
}}
diff --git a/docs/examples/validate-perf.tsx b/docs/examples/validate-perf.tsx
index bd9ea48a..ca146bd7 100644
--- a/docs/examples/validate-perf.tsx
+++ b/docs/examples/validate-perf.tsx
@@ -34,6 +34,10 @@ export default class Demo extends React.Component {
console.log('Failed:', errorInfo);
};
+ public onPasswordError = (errors: string[]) => {
+ console.log('π Password Error:', errors);
+ };
+
public render() {
return (
@@ -49,7 +53,18 @@ export default class Demo extends React.Component {
{
+ if (value.length < 6) {
+ throw new Error('δ½ η ${displayName} ε€ͺηδΊβ¦β¦');
+ }
+ },
+ },
+ ]}
+ onError={this.onPasswordError}
>
@@ -118,6 +133,7 @@ export default class Demo extends React.Component {
>
Reset
+
);
diff --git a/docs/examples/validate.tsx b/docs/examples/validate.tsx
index ceaf7226..1721e2dc 100644
--- a/docs/examples/validate.tsx
+++ b/docs/examples/validate.tsx
@@ -33,139 +33,141 @@ const FieldState = ({ form, name }) => {
);
};
-export default class Demo extends React.Component {
- onFinish = values => {
+export default () => {
+ const onFinish = (values: object) => {
console.log('Finish:', values);
};
- render() {
- return (
-
+ );
+};
diff --git a/src/Field.tsx b/src/Field.tsx
index c2d211c8..3074c357 100644
--- a/src/Field.tsx
+++ b/src/Field.tsx
@@ -1,7 +1,7 @@
import toChildrenArray from 'rc-util/lib/Children/toArray';
import warning from 'rc-util/lib/warning';
import * as React from 'react';
-import {
+import type {
FieldEntity,
FormInstance,
InternalNamePath,
@@ -15,6 +15,7 @@ import {
RuleObject,
StoreValue,
EventArgs,
+ RuleError,
} from './interface';
import FieldContext, { HOOK_MARK } from './FieldContext';
import { toArray } from './utils/typeUtil';
@@ -24,8 +25,11 @@ import {
defaultGetValueFromEvent,
getNamePath,
getValue,
+ isSimilar,
} from './utils/valueUtil';
+const EMPTY_ERRORS: any[] = [];
+
export type ShouldUpdate =
| boolean
| ((prevValues: Values, nextValues: Values, info: { source?: string }) => boolean);
@@ -44,6 +48,7 @@ function requireUpdate(
return prevValue !== nextValue;
}
+// eslint-disable-next-line @typescript-eslint/consistent-indexed-object-style
interface ChildProps {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[name: string]: any;
@@ -72,6 +77,7 @@ export interface InternalFieldProps {
messageVariables?: Record;
initialValue?: any;
onReset?: () => void;
+ onError?: (errors: string[], warnings: string[]) => void;
preserve?: boolean;
/** @private Passed by Form.List props. Do not use since it will break by path check. */
@@ -128,7 +134,8 @@ class Field extends React.Component implements F
private prevValidating: boolean;
- private errors: string[] = [];
+ private errors: string[] = EMPTY_ERRORS;
+ private warnings: string[] = EMPTY_ERRORS;
// ============================== Subscriptions ==============================
constructor(props: InternalFieldProps) {
@@ -185,14 +192,12 @@ class Field extends React.Component implements F
public getRules = (): RuleObject[] => {
const { rules = [], fieldContext } = this.props;
- return rules.map(
- (rule: Rule): RuleObject => {
- if (typeof rule === 'function') {
- return rule(fieldContext);
- }
- return rule;
- },
- );
+ return rules.map((rule: Rule): RuleObject => {
+ if (typeof rule === 'function') {
+ return rule(fieldContext);
+ }
+ return rule;
+ });
};
public reRender() {
@@ -211,6 +216,21 @@ class Field extends React.Component implements F
}));
};
+ /** Update `this.error`. If `onError` provided, trigger it */
+ public updateError(
+ prevErrors: string[],
+ nextErrors: string[],
+ prevWarnings: string[],
+ nextWarnings: string[],
+ ) {
+ const { onError } = this.props;
+ if (onError && (!isSimilar(prevErrors, nextErrors) || !isSimilar(prevWarnings, nextWarnings))) {
+ onError(nextErrors, nextWarnings);
+ }
+ this.errors = nextErrors;
+ this.warnings = nextWarnings;
+ }
+
// ========================= Field Entity Interfaces =========================
// Trigger by store update. Check if need update the component
public onStoreChange: FieldEntity['onStoreChange'] = (prevStore, namePathList, info) => {
@@ -222,12 +242,15 @@ class Field extends React.Component implements F
const namePathMatch = namePathList && containsNamePath(namePathList, namePath);
+ const prevErrors = this.errors;
+ const prevWarnings = this.warnings;
+
// `setFieldsValue` is a quick access to update related status
if (info.type === 'valueUpdate' && info.source === 'external' && prevValue !== curValue) {
this.touched = true;
this.dirty = true;
this.validatePromise = null;
- this.errors = [];
+ this.updateError(prevErrors, EMPTY_ERRORS, prevWarnings, EMPTY_ERRORS);
}
switch (info.type) {
@@ -237,11 +260,9 @@ class Field extends React.Component implements F
this.touched = false;
this.dirty = false;
this.validatePromise = null;
- this.errors = [];
+ this.updateError(prevErrors, EMPTY_ERRORS, prevWarnings, EMPTY_ERRORS);
- if (onReset) {
- onReset();
- }
+ onReset?.();
this.refresh();
return;
@@ -257,8 +278,13 @@ class Field extends React.Component implements F
if ('validating' in data && !('originRCField' in data)) {
this.validatePromise = data.validating ? Promise.resolve([]) : null;
}
- if ('errors' in data) {
- this.errors = data.errors || [];
+
+ const hasError = 'errors' in data;
+ const hasWarning = 'warnings' in data;
+ if (hasError || hasWarning) {
+ const nextErrors = hasError ? data.errors || EMPTY_ERRORS : prevErrors;
+ const nextWarnings = hasWarning ? data.warnings || EMPTY_ERRORS : prevWarnings;
+ this.updateError(prevErrors, nextErrors, prevWarnings, nextWarnings);
}
this.dirty = true;
@@ -320,7 +346,10 @@ class Field extends React.Component implements F
}
};
- public validateRules = (options?: ValidateOptions): Promise => {
+ public validateRules = (options?: ValidateOptions): Promise => {
+ const prevErrors = this.errors;
+ const prevWarnings = this.warnings;
+
// We should fixed namePath & value to avoid developer change then by form function
const namePath = this.getNamePath();
const currentValue = this.getValue();
@@ -357,10 +386,23 @@ class Field extends React.Component implements F
promise
.catch(e => e)
- .then((errors: string[] = []) => {
+ .then((ruleErrors: RuleError[] = EMPTY_ERRORS) => {
if (this.validatePromise === rootPromise) {
this.validatePromise = null;
- this.errors = errors;
+
+ // Get errors & warnings
+ const nextErrors: string[] = [];
+ const nextWarnings: string[] = [];
+ ruleErrors.forEach(({ rule: { warningOnly }, errors = EMPTY_ERRORS }) => {
+ if (warningOnly) {
+ nextWarnings.push(...errors);
+ } else {
+ nextErrors.push(...errors);
+ }
+ });
+
+ this.updateError(prevErrors, nextErrors, prevWarnings, nextWarnings);
+
this.reRender();
}
});
@@ -370,7 +412,8 @@ class Field extends React.Component implements F
this.validatePromise = rootPromise;
this.dirty = true;
- this.errors = [];
+ this.errors = EMPTY_ERRORS;
+ this.warnings = EMPTY_ERRORS;
// Force trigger re-render since we need sync renderProps with new meta
this.reRender();
@@ -386,6 +429,8 @@ class Field extends React.Component implements F
public getErrors = () => this.errors;
+ public getWarnings = () => this.warnings;
+
public isListField = () => this.props.isListField;
public isList = () => this.props.isList;
@@ -401,6 +446,7 @@ class Field extends React.Component implements F
touched: this.isFieldTouched(),
validating: this.prevValidating,
errors: this.errors,
+ warnings: this.warnings,
name: this.getNamePath(),
};
diff --git a/src/FieldContext.ts b/src/FieldContext.ts
index 6ddc191f..5429aaa9 100644
--- a/src/FieldContext.ts
+++ b/src/FieldContext.ts
@@ -1,6 +1,6 @@
import * as React from 'react';
import warning from 'rc-util/lib/warning';
-import { InternalFormInstance } from './interface';
+import type { InternalFormInstance } from './interface';
export const HOOK_MARK = 'RC_FORM_INTERNAL_HOOKS';
@@ -13,6 +13,7 @@ const Context = React.createContext({
getFieldValue: warningFunc,
getFieldsValue: warningFunc,
getFieldError: warningFunc,
+ getFieldWarning: warningFunc,
getFieldsError: warningFunc,
isFieldsTouched: warningFunc,
isFieldTouched: warningFunc,
diff --git a/src/interface.ts b/src/interface.ts
index 3fdbf4a1..45dcff6e 100644
--- a/src/interface.ts
+++ b/src/interface.ts
@@ -1,18 +1,17 @@
-import { ReactElement } from 'react';
-import { ReducerAction } from './useForm';
+import type { ReactElement } from 'react';
+import type { ReducerAction } from './useForm';
export type InternalNamePath = (string | number)[];
export type NamePath = string | number | InternalNamePath;
export type StoreValue = any;
-export interface Store {
- [name: string]: StoreValue;
-}
+export type Store = Record;
export interface Meta {
touched: boolean;
validating: boolean;
errors: string[];
+ warnings: string[];
name: InternalNamePath;
}
@@ -51,11 +50,13 @@ type Validator = (
export type RuleRender = (form: FormInstance) => RuleObject;
export interface ValidatorRule {
+ warningOnly?: boolean;
message?: string | ReactElement;
validator: Validator;
}
interface BaseRule {
+ warningOnly?: boolean;
enum?: StoreValue[];
len?: number;
max?: number;
@@ -100,10 +101,11 @@ export interface FieldEntity {
isListField: () => boolean;
isList: () => boolean;
isPreserve: () => boolean;
- validateRules: (options?: ValidateOptions) => Promise;
+ validateRules: (options?: ValidateOptions) => Promise;
getMeta: () => Meta;
getNamePath: () => InternalNamePath;
getErrors: () => string[];
+ getWarnings: () => string[];
props: {
name?: NamePath;
rules?: Rule[];
@@ -115,6 +117,12 @@ export interface FieldEntity {
export interface FieldError {
name: InternalNamePath;
errors: string[];
+ warnings: string[];
+}
+
+export interface RuleError {
+ errors: string[];
+ rule: RuleObject;
}
export interface ValidateOptions {
@@ -210,6 +218,7 @@ export interface FormInstance {
getFieldsValue(nameList: NamePath[] | true, filterFunc?: (meta: Meta) => boolean): any;
getFieldError: (name: NamePath) => string[];
getFieldsError: (nameList?: NamePath[]) => FieldError[];
+ getFieldWarning: (name: NamePath) => string[];
isFieldsTouched(nameList?: NamePath[], allFieldsTouched?: boolean): boolean;
isFieldsTouched(allFieldsTouched?: boolean): boolean;
isFieldTouched: (name: NamePath) => boolean;
diff --git a/src/useForm.ts b/src/useForm.ts
index cb620f8f..b6037a48 100644
--- a/src/useForm.ts
+++ b/src/useForm.ts
@@ -20,6 +20,7 @@ import type {
Meta,
InternalFieldData,
ValuedNotifyInfo,
+ RuleError,
} from './interface';
import { HOOK_MARK } from './FieldContext';
import { allPromiseFinish } from './utils/asyncUtil';
@@ -80,6 +81,7 @@ export class FormStore {
getFieldValue: this.getFieldValue,
getFieldsValue: this.getFieldsValue,
getFieldError: this.getFieldError,
+ getFieldWarning: this.getFieldWarning,
getFieldsError: this.getFieldsError,
isFieldsTouched: this.isFieldsTouched,
isFieldTouched: this.isFieldTouched,
@@ -249,12 +251,14 @@ export class FormStore {
return {
name: entity.getNamePath(),
errors: entity.getErrors(),
+ warnings: entity.getWarnings(),
};
}
return {
name: getNamePath(nameList[index]),
errors: [],
+ warnings: [],
};
});
};
@@ -267,6 +271,14 @@ export class FormStore {
return fieldError.errors;
};
+ private getFieldWarning = (name: NamePath): string[] => {
+ this.warningUnhooked();
+
+ const namePath = getNamePath(name);
+ const fieldError = this.getFieldsError([namePath])[0];
+ return fieldError.warnings;
+ };
+
private isFieldsTouched = (...args) => {
this.warningUnhooked();
@@ -484,23 +496,21 @@ export class FormStore {
private getFields = (): InternalFieldData[] => {
const entities = this.getFieldEntities(true);
- const fields = entities.map(
- (field: FieldEntity): InternalFieldData => {
- const namePath = field.getNamePath();
- const meta = field.getMeta();
- const fieldData = {
- ...meta,
- name: namePath,
- value: this.getFieldValue(namePath),
- };
+ const fields = entities.map((field: FieldEntity): InternalFieldData => {
+ const namePath = field.getNamePath();
+ const meta = field.getMeta();
+ const fieldData = {
+ ...meta,
+ name: namePath,
+ value: this.getFieldValue(namePath),
+ };
- Object.defineProperty(fieldData, 'originRCField', {
- value: true,
- });
+ Object.defineProperty(fieldData, 'originRCField', {
+ value: true,
+ });
- return fieldData;
- },
- );
+ return fieldData;
+ });
return fields;
};
@@ -731,10 +741,7 @@ export class FormStore {
: [];
// Collect result in promise list
- const promiseList: Promise<{
- name: InternalNamePath;
- errors: string[];
- }>[] = [];
+ const promiseList: Promise[] = [];
this.getFieldEntities(true).forEach((field: FieldEntity) => {
// Add field if not provide `nameList`
@@ -776,13 +783,33 @@ export class FormStore {
// Wrap promise with field
promiseList.push(
promise
- .then(() => ({ name: fieldNamePath, errors: [] }))
- .catch(errors =>
- Promise.reject({
+ .then(() => ({ name: fieldNamePath, errors: [], warnings: [] }))
+ .catch((ruleErrors: RuleError[]) => {
+ const mergedErrors: string[] = [];
+ const mergedWarnings: string[] = [];
+
+ ruleErrors.forEach(({ rule: { warningOnly }, errors }) => {
+ if (warningOnly) {
+ mergedWarnings.push(...errors);
+ } else {
+ mergedErrors.push(...errors);
+ }
+ });
+
+ if (mergedErrors.length) {
+ return Promise.reject({
+ name: fieldNamePath,
+ errors: mergedErrors,
+ warnings: mergedWarnings,
+ });
+ }
+
+ return {
name: fieldNamePath,
- errors,
- }),
- ),
+ errors: mergedErrors,
+ warnings: mergedWarnings,
+ };
+ }),
);
}
});
@@ -802,14 +829,12 @@ export class FormStore {
});
const returnPromise: Promise = summaryPromise
- .then(
- (): Promise => {
- if (this.lastValidatePromise === summaryPromise) {
- return Promise.resolve(this.getFieldsValue(namePathList));
- }
- return Promise.reject([]);
- },
- )
+ .then((): Promise => {
+ if (this.lastValidatePromise === summaryPromise) {
+ return Promise.resolve(this.getFieldsValue(namePathList));
+ }
+ return Promise.reject([]);
+ })
.catch((results: { name: InternalNamePath; errors: string[] }[]) => {
const errorList = results.filter(result => result && result.errors.length);
return Promise.reject({
diff --git a/src/utils/asyncUtil.ts b/src/utils/asyncUtil.ts
index 3a977087..51593b5f 100644
--- a/src/utils/asyncUtil.ts
+++ b/src/utils/asyncUtil.ts
@@ -1,4 +1,4 @@
-import { FieldError } from '../interface';
+import type { FieldError } from '../interface';
export function allPromiseFinish(promiseList: Promise[]): Promise {
let hasError = false;
diff --git a/src/utils/validateUtil.ts b/src/utils/validateUtil.ts
index 1c702aae..79dccc1a 100644
--- a/src/utils/validateUtil.ts
+++ b/src/utils/validateUtil.ts
@@ -4,12 +4,12 @@ import warning from 'rc-util/lib/warning';
import type {
InternalNamePath,
ValidateOptions,
- ValidateMessages,
RuleObject,
StoreValue,
+ RuleError,
} from '../interface';
-import { setValues } from './valueUtil';
import { defaultValidateMessages } from './messages';
+import { setValues } from './valueUtil';
// Remove incorrect original ts define
const AsyncValidator: any = RawAsyncValidator;
@@ -25,46 +25,6 @@ function replaceMessage(template: string, kv: Record): string {
});
}
-/**
- * We use `async-validator` to validate rules. So have to hot replace the message with validator.
- * { required: '${name} is required' } => { required: () => 'field is required' }
- */
-function convertMessages(
- messages: ValidateMessages,
- name: string,
- rule: RuleObject,
- messageVariables?: Record,
-): ValidateMessages {
- const kv = {
- ...(rule as Record),
- name,
- enum: (rule.enum || []).join(', '),
- };
-
- const replaceFunc = (template: string, additionalKV?: Record) => () =>
- replaceMessage(template, { ...kv, ...additionalKV });
-
- /* eslint-disable no-param-reassign */
- function fillTemplate(source: ValidateMessages, target: ValidateMessages = {}) {
- Object.keys(source).forEach(ruleName => {
- const value = source[ruleName];
- if (typeof value === 'string') {
- target[ruleName] = replaceFunc(value, messageVariables);
- } else if (value && typeof value === 'object') {
- target[ruleName] = {};
- fillTemplate(value, target[ruleName]);
- } else {
- target[ruleName] = value;
- }
- });
-
- return target;
- }
- /* eslint-enable */
-
- return fillTemplate(setValues({}, defaultValidateMessages, messages)) as ValidateMessages;
-}
-
async function validateRule(
name: string,
value: StoreValue,
@@ -84,12 +44,7 @@ async function validateRule(
[name]: [cloneRule],
});
- const messages: ValidateMessages = convertMessages(
- options.validateMessages,
- name,
- cloneRule,
- messageVariables,
- );
+ const messages = setValues({}, defaultValidateMessages, options.validateMessages);
validator.messages(messages);
let result = [];
@@ -106,7 +61,7 @@ async function validateRule(
);
} else {
console.error(errObj);
- result = [(messages.default as () => string)()];
+ result = [messages.default];
}
}
@@ -120,7 +75,22 @@ async function validateRule(
return subResults.reduce((prev, errors) => [...prev, ...errors], []);
}
- return result;
+ // Replace message with variables
+ const kv = {
+ ...(rule as Record),
+ name,
+ enum: (rule.enum || []).join(', '),
+ ...messageVariables,
+ };
+
+ const fillVariableResult = result.map(error => {
+ if (typeof error === 'string') {
+ return replaceMessage(error, kv);
+ }
+ return error;
+ });
+
+ return fillVariableResult;
}
/**
@@ -138,66 +108,88 @@ export function validateRules(
const name = namePath.join('.');
// Fill rule with context
- const filledRules: RuleObject[] = rules.map(currentRule => {
- const originValidatorFunc = currentRule.validator;
-
- if (!originValidatorFunc) {
- return currentRule;
- }
- return {
- ...currentRule,
- validator(rule: RuleObject, val: StoreValue, callback: (error?: string) => void) {
- let hasPromise = false;
-
- // Wrap callback only accept when promise not provided
- const wrappedCallback = (...args: string[]) => {
- // Wait a tick to make sure return type is a promise
- Promise.resolve().then(() => {
- warning(
- !hasPromise,
- 'Your validator function has already return a promise. `callback` will be ignored.',
- );
-
- if (!hasPromise) {
- callback(...args);
- }
- });
+ const filledRules: RuleObject[] = rules
+ .map((currentRule, ruleIndex) => {
+ const originValidatorFunc = currentRule.validator;
+ const cloneRule = {
+ ...currentRule,
+ ruleIndex,
+ };
+
+ // Replace validator if needed
+ if (originValidatorFunc) {
+ cloneRule.validator = (
+ rule: RuleObject,
+ val: StoreValue,
+ callback: (error?: string) => void,
+ ) => {
+ let hasPromise = false;
+
+ // Wrap callback only accept when promise not provided
+ const wrappedCallback = (...args: string[]) => {
+ // Wait a tick to make sure return type is a promise
+ Promise.resolve().then(() => {
+ warning(
+ !hasPromise,
+ 'Your validator function has already return a promise. `callback` will be ignored.',
+ );
+
+ if (!hasPromise) {
+ callback(...args);
+ }
+ });
+ };
+
+ // Get promise
+ const promise = originValidatorFunc(rule, val, wrappedCallback);
+ hasPromise =
+ promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
+
+ /**
+ * 1. Use promise as the first priority.
+ * 2. If promise not exist, use callback with warning instead
+ */
+ warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
+
+ if (hasPromise) {
+ (promise as Promise)
+ .then(() => {
+ callback();
+ })
+ .catch(err => {
+ callback(err || ' ');
+ });
+ }
};
+ }
- // Get promise
- const promise = originValidatorFunc(rule, val, wrappedCallback);
- hasPromise =
- promise && typeof promise.then === 'function' && typeof promise.catch === 'function';
-
- /**
- * 1. Use promise as the first priority.
- * 2. If promise not exist, use callback with warning instead
- */
- warning(hasPromise, '`callback` is deprecated. Please return a promise instead.');
-
- if (hasPromise) {
- (promise as Promise)
- .then(() => {
- callback();
- })
- .catch(err => {
- callback(err || ' ');
- });
- }
- },
- };
- });
+ return cloneRule;
+ })
+ .sort(({ warningOnly: w1, ruleIndex: i1 }, { warningOnly: w2, ruleIndex: i2 }) => {
+ if (!!w1 === !!w2) {
+ // Let keep origin order
+ return i1 - i2;
+ }
- let summaryPromise: Promise;
+ if (w1) {
+ return 1;
+ }
+
+ return -1;
+ });
+
+ // Do validate rules
+ let summaryPromise: Promise;
if (validateFirst === true) {
// >>>>> Validate by serialization
summaryPromise = new Promise(async (resolve, reject) => {
/* eslint-disable no-await-in-loop */
for (let i = 0; i < filledRules.length; i += 1) {
- const errors = await validateRule(name, value, filledRules[i], options, messageVariables);
+ const rule = filledRules[i];
+ const errors = await validateRule(name, value, rule, options, messageVariables);
if (errors.length) {
- reject(errors);
+ reject([{ errors, rule }]);
return;
}
}
@@ -207,19 +199,15 @@ export function validateRules(
});
} else {
// >>>>> Validate by parallel
- const rulePromises = filledRules.map(rule =>
- validateRule(name, value, rule, options, messageVariables),
+ const rulePromises: Promise[] = filledRules.map(rule =>
+ validateRule(name, value, rule, options, messageVariables).then(errors => ({ errors, rule })),
);
- summaryPromise = (validateFirst
- ? finishOnFirstFailed(rulePromises)
- : finishOnAllFailed(rulePromises)
- ).then((errors: string[]): string[] | Promise => {
- if (!errors.length) {
- return [];
- }
-
- return Promise.reject(errors);
+ summaryPromise = (
+ validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)
+ ).then((errors: RuleError[]): RuleError[] | Promise => {
+ // Always change to rejection for Field to catch
+ return Promise.reject(errors);
});
}
@@ -229,22 +217,24 @@ export function validateRules(
return summaryPromise;
}
-async function finishOnAllFailed(rulePromises: Promise[]): Promise {
- return Promise.all(rulePromises).then((errorsList: string[][]): string[] | Promise => {
- const errors: string[] = [].concat(...errorsList);
+async function finishOnAllFailed(rulePromises: Promise[]): Promise {
+ return Promise.all(rulePromises).then((errorsList: RuleError[]):
+ | RuleError[]
+ | Promise => {
+ const errors: RuleError[] = [].concat(...errorsList);
return errors;
});
}
-async function finishOnFirstFailed(rulePromises: Promise[]): Promise {
+async function finishOnFirstFailed(rulePromises: Promise[]): Promise {
let count = 0;
return new Promise(resolve => {
rulePromises.forEach(promise => {
- promise.then(errors => {
- if (errors.length) {
- resolve(errors);
+ promise.then(ruleError => {
+ if (ruleError.errors.length) {
+ resolve([ruleError]);
}
count += 1;
diff --git a/tests/common/InfoField.tsx b/tests/common/InfoField.tsx
index 54613067..ecf75ecd 100644
--- a/tests/common/InfoField.tsx
+++ b/tests/common/InfoField.tsx
@@ -1,9 +1,9 @@
-import React, { ReactElement } from 'react';
+import React from 'react';
import { Field } from '../../src';
-import { FieldProps } from '../../src/Field';
+import type { FieldProps } from '../../src/Field';
interface InfoFieldProps extends FieldProps {
- children?: ReactElement;
+ children?: React.ReactElement;
}
export const Input = ({ value = '', ...props }) => ;
@@ -14,7 +14,7 @@ export const Input = ({ value = '', ...props }) => = ({ children, ...props }) => (
{(control, info) => {
- const { errors, validating } = info;
+ const { errors, warnings, validating } = info;
return (
@@ -24,6 +24,11 @@ const InfoField: React.FC
= ({ children, ...props }) => (
- {error}
))}
+
+ {warnings.map((warning, index) => (
+ - {warning}
+ ))}
+
{validating && }
);
diff --git a/tests/common/index.ts b/tests/common/index.ts
index 4d9b3cdd..54dc9073 100644
--- a/tests/common/index.ts
+++ b/tests/common/index.ts
@@ -1,6 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { act } from 'react-dom/test-utils';
+import type { ReactWrapper } from 'enzyme';
import timeout from './timeout';
import { Field } from '../../src';
import { getNamePath, matchNamePath } from '../../src/utils/valueUtil';
@@ -13,7 +14,12 @@ export async function changeValue(wrapper, value) {
wrapper.update();
}
-export function matchError(wrapper, error) {
+export function matchError(
+ wrapper: ReactWrapper,
+ error?: boolean | string,
+ warning?: boolean | string,
+) {
+ // Error
if (error) {
expect(wrapper.find('.errors li').length).toBeTruthy();
} else {
@@ -23,6 +29,17 @@ export function matchError(wrapper, error) {
if (error && typeof error !== 'boolean') {
expect(wrapper.find('.errors li').text()).toBe(error);
}
+
+ // Warning
+ if (warning) {
+ expect(wrapper.find('.warnings li').length).toBeTruthy();
+ } else {
+ expect(wrapper.find('.warnings li').length).toBeFalsy();
+ }
+
+ if (warning && typeof warning !== 'boolean') {
+ expect(wrapper.find('.warnings li').text()).toBe(warning);
+ }
}
export function getField(wrapper, index: string | number = 0) {
diff --git a/tests/context.test.js b/tests/context.test.js
index 0cdc0f52..d4a5c193 100644
--- a/tests/context.test.js
+++ b/tests/context.test.js
@@ -35,7 +35,14 @@ describe('Form.Context', () => {
'form1',
expect.objectContaining({
changedFields: [
- { errors: [], name: ['username'], touched: true, validating: false, value: 'Light' },
+ {
+ errors: [],
+ warnings: [],
+ name: ['username'],
+ touched: true,
+ validating: false,
+ value: 'Light',
+ },
],
forms: {
form1: expect.objectContaining({}),
diff --git a/tests/index.test.js b/tests/index.test.js
index 419fd0c5..030ba010 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -101,6 +101,7 @@ describe('Form.Basic', () => {
it(name, async () => {
let form;
const onReset = jest.fn();
+ const onError = jest.fn();
const wrapper = mount(
@@ -109,7 +110,12 @@ describe('Form.Basic', () => {
form = instance;
}}
>
-
+
@@ -120,25 +126,32 @@ describe('Form.Basic', () => {
expect(form.getFieldValue('username')).toEqual('Bamboo');
expect(form.getFieldError('username')).toEqual([]);
expect(form.isFieldTouched('username')).toBeTruthy();
-
+ expect(onError).not.toHaveBeenCalled();
expect(onReset).not.toHaveBeenCalled();
+
form.resetFields(...args);
expect(form.getFieldValue('username')).toEqual(undefined);
expect(form.getFieldError('username')).toEqual([]);
expect(form.isFieldTouched('username')).toBeFalsy();
+ expect(onError).not.toHaveBeenCalled();
expect(onReset).toHaveBeenCalled();
+ onError.mockRestore();
onReset.mockRestore();
await changeValue(getField(wrapper, 'username'), '');
expect(form.getFieldValue('username')).toEqual('');
expect(form.getFieldError('username')).toEqual(["'username' is required"]);
expect(form.isFieldTouched('username')).toBeTruthy();
-
+ expect(onError).toHaveBeenCalledWith(["'username' is required"], []);
expect(onReset).not.toHaveBeenCalled();
+ onError.mockRestore();
+ onReset.mockRestore();
+
form.resetFields(...args);
expect(form.getFieldValue('username')).toEqual(undefined);
expect(form.getFieldError('username')).toEqual([]);
expect(form.isFieldTouched('username')).toBeFalsy();
+ expect(onError).toHaveBeenCalledWith([], []);
expect(onReset).toHaveBeenCalled();
});
}
@@ -277,7 +290,7 @@ describe('Form.Basic', () => {
matchError(wrapper, "'user' is required");
expect(onFinish).not.toHaveBeenCalled();
expect(onFinishFailed).toHaveBeenCalledWith({
- errorFields: [{ name: ['user'], errors: ["'user' is required"] }],
+ errorFields: [{ name: ['user'], errors: ["'user' is required"], warnings: [] }],
outOfDate: false,
values: {},
});
@@ -633,7 +646,7 @@ describe('Form.Basic', () => {
expect(
form.getFieldsValue(null, meta => {
- expect(Object.keys(meta)).toEqual(['touched', 'validating', 'errors', 'name']);
+ expect(Object.keys(meta)).toEqual(['touched', 'validating', 'errors', 'warnings', 'name']);
return false;
}),
).toEqual({});
diff --git a/tests/validate-warning.test.tsx b/tests/validate-warning.test.tsx
new file mode 100644
index 00000000..447153c3
--- /dev/null
+++ b/tests/validate-warning.test.tsx
@@ -0,0 +1,93 @@
+/* eslint-disable no-template-curly-in-string */
+import React from 'react';
+import { mount } from 'enzyme';
+import Form from '../src';
+import InfoField, { Input } from './common/InfoField';
+import { changeValue, matchError } from './common';
+import type { FormInstance, Rule } from '../src/interface';
+
+describe('Form.WarningValidate', () => {
+ it('required', async () => {
+ let form: FormInstance;
+
+ const wrapper = mount(
+ ,
+ );
+
+ await changeValue(wrapper, '');
+ matchError(wrapper, false, "'name' is required");
+ expect(form.getFieldWarning('name')).toEqual(["'name' is required"]);
+ });
+
+ describe('validateFirst should not block error', () => {
+ function testValidateFirst(
+ name: string,
+ validateFirst: boolean | 'parallel',
+ additionalRule?: Rule,
+ errorMessage?: string,
+ ) {
+ it(name, async () => {
+ const rules = [
+ additionalRule,
+ {
+ type: 'string',
+ len: 10,
+ warningOnly: true,
+ },
+ {
+ type: 'url',
+ },
+ {
+ type: 'string',
+ len: 20,
+ warningOnly: true,
+ },
+ ];
+
+ const wrapper = mount(
+ ,
+ );
+
+ await changeValue(wrapper, 'bamboo');
+ matchError(wrapper, errorMessage || "'name' is not a valid url", false);
+ });
+ }
+
+ testValidateFirst('default', true);
+ testValidateFirst(
+ 'default',
+ true,
+ {
+ type: 'string',
+ len: 3,
+ },
+ "'name' must be exactly 3 characters",
+ );
+ testValidateFirst('parallel', 'parallel');
+ });
+});
+/* eslint-enable no-template-curly-in-string */
diff --git a/tests/validate.test.tsx b/tests/validate.test.tsx
index 0d9bd7ee..76ca9a89 100644
--- a/tests/validate.test.tsx
+++ b/tests/validate.test.tsx
@@ -29,6 +29,7 @@ describe('Form.Validate', () => {
{
name: ['username'],
errors: ["'username' is required"],
+ warnings: [],
},
]);
@@ -37,10 +38,12 @@ describe('Form.Validate', () => {
{
name: ['username'],
errors: ["'username' is required"],
+ warnings: [],
},
{
name: ['not-exist'],
errors: [],
+ warnings: [],
},
]);
});
@@ -412,7 +415,7 @@ describe('Form.Validate', () => {
})
.then(() => {
expect(failed).toBeTruthy();
- resolve();
+ resolve('');
});
});
});
@@ -465,7 +468,7 @@ describe('Form.Validate', () => {
validator: () =>
new Promise(resolve => {
if (canEnd) {
- resolve();
+ resolve('');
}
}),
},
@@ -483,6 +486,7 @@ describe('Form.Validate', () => {
{
name: ['username'],
errors: ["'username' is required"],
+ warnings: [],
},
]);
expect(onFinish).not.toHaveBeenCalled();