fix: form validateFirst not reject #4273
parent
4a0fce5a0a
commit
5f8d8a555b
|
@ -16,7 +16,13 @@ import initDefaultProps from '../_util/props-util/initDefaultProps';
|
||||||
import type { VueNode } from '../_util/type';
|
import type { VueNode } from '../_util/type';
|
||||||
import { tuple } from '../_util/type';
|
import { tuple } from '../_util/type';
|
||||||
import type { ColProps } from '../grid/Col';
|
import type { ColProps } from '../grid/Col';
|
||||||
import type { InternalNamePath, NamePath, ValidateErrorEntity, ValidateOptions } from './interface';
|
import type {
|
||||||
|
InternalNamePath,
|
||||||
|
NamePath,
|
||||||
|
RuleError,
|
||||||
|
ValidateErrorEntity,
|
||||||
|
ValidateOptions,
|
||||||
|
} from './interface';
|
||||||
import { useInjectSize } from '../_util/hooks/useSize';
|
import { useInjectSize } from '../_util/hooks/useSize';
|
||||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||||
import { useProvideForm } from './context';
|
import { useProvideForm } from './context';
|
||||||
|
@ -247,13 +253,33 @@ const Form = defineComponent({
|
||||||
// Wrap promise with field
|
// Wrap promise with field
|
||||||
promiseList.push(
|
promiseList.push(
|
||||||
promise
|
promise
|
||||||
.then(() => ({ name: fieldNamePath, errors: [] }))
|
.then<any, RuleError>(() => ({ name: fieldNamePath, errors: [], warnings: [] }))
|
||||||
.catch((errors: any) =>
|
.catch((ruleErrors: RuleError[]) => {
|
||||||
Promise.reject({
|
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,
|
name: fieldNamePath,
|
||||||
errors,
|
errors: mergedErrors,
|
||||||
}),
|
warnings: mergedWarnings,
|
||||||
),
|
};
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { toArray } from './utils/typeUtil';
|
||||||
import { warning } from '../vc-util/warning';
|
import { warning } from '../vc-util/warning';
|
||||||
import find from 'lodash-es/find';
|
import find from 'lodash-es/find';
|
||||||
import { tuple } from '../_util/type';
|
import { tuple } from '../_util/type';
|
||||||
import type { InternalNamePath, RuleObject, ValidateOptions } from './interface';
|
import type { InternalNamePath, RuleError, RuleObject, ValidateOptions } from './interface';
|
||||||
import useConfigInject from '../_util/hooks/useConfigInject';
|
import useConfigInject from '../_util/hooks/useConfigInject';
|
||||||
import { useInjectForm } from './context';
|
import { useInjectForm } from './context';
|
||||||
import FormItemLabel from './FormItemLabel';
|
import FormItemLabel from './FormItemLabel';
|
||||||
|
@ -31,7 +31,7 @@ export interface FieldExpose {
|
||||||
clearValidate: () => void;
|
clearValidate: () => void;
|
||||||
namePath: ComputedRef<InternalNamePath>;
|
namePath: ComputedRef<InternalNamePath>;
|
||||||
rules?: ComputedRef<ValidationRule[]>;
|
rules?: ComputedRef<ValidationRule[]>;
|
||||||
validateRules: (options: ValidateOptions) => Promise<void> | Promise<string[]>;
|
validateRules: (options: ValidateOptions) => Promise<void> | Promise<RuleError[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPropByPath(obj: any, namePathList: any, strict?: boolean) {
|
function getPropByPath(obj: any, namePathList: any, strict?: boolean) {
|
||||||
|
@ -209,10 +209,12 @@ export default defineComponent({
|
||||||
|
|
||||||
promise
|
promise
|
||||||
.catch(e => e)
|
.catch(e => e)
|
||||||
.then((ers = []) => {
|
.then((results: RuleError[] = []) => {
|
||||||
if (validateState.value === 'validating') {
|
if (validateState.value === 'validating') {
|
||||||
validateState.value = ers.length ? 'error' : 'success';
|
const res = results.filter(result => result && result.errors.length);
|
||||||
errors.value = ers;
|
validateState.value = res.length ? 'error' : 'success';
|
||||||
|
|
||||||
|
errors.value = res.map(r => r.errors);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -50,11 +50,13 @@ type Validator = (
|
||||||
) => Promise<void> | void;
|
) => Promise<void> | void;
|
||||||
|
|
||||||
export interface ValidatorRule {
|
export interface ValidatorRule {
|
||||||
|
warningOnly?: boolean;
|
||||||
message?: string | VueNode;
|
message?: string | VueNode;
|
||||||
validator: Validator;
|
validator: Validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface BaseRule {
|
interface BaseRule {
|
||||||
|
warningOnly?: boolean;
|
||||||
enum?: StoreValue[];
|
enum?: StoreValue[];
|
||||||
len?: number;
|
len?: number;
|
||||||
max?: number;
|
max?: number;
|
||||||
|
@ -92,6 +94,11 @@ export interface FieldError {
|
||||||
errors: string[];
|
errors: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RuleError {
|
||||||
|
errors: string[];
|
||||||
|
rule: RuleObject;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ValidateOptions {
|
export interface ValidateOptions {
|
||||||
triggerName?: string;
|
triggerName?: string;
|
||||||
validateMessages?: ValidateMessages;
|
validateMessages?: ValidateMessages;
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import type { FieldError } from '../interface';
|
import type { FieldError } from '../interface';
|
||||||
|
|
||||||
export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
|
export function allPromiseFinish(promiseList: Promise<FieldError>[]): Promise<FieldError[]> {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
let count = promiseList.length;
|
let count = promiseList.length;
|
||||||
const results = [];
|
const results: FieldError[] = [];
|
||||||
|
|
||||||
if (!promiseList.length) {
|
if (!promiseList.length) {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { warning } from '../../vc-util/warning';
|
||||||
import { setValues } from './valueUtil';
|
import { setValues } from './valueUtil';
|
||||||
import { defaultValidateMessages } from './messages';
|
import { defaultValidateMessages } from './messages';
|
||||||
import { isValidElement } from '../../_util/props-util';
|
import { isValidElement } from '../../_util/props-util';
|
||||||
import type { InternalNamePath, RuleObject, ValidateMessages, ValidateOptions } from '../interface';
|
import type { InternalNamePath, RuleError, RuleObject, ValidateOptions } from '../interface';
|
||||||
|
|
||||||
// Remove incorrect original ts define
|
// Remove incorrect original ts define
|
||||||
const AsyncValidator: any = RawAsyncValidator;
|
const AsyncValidator: any = RawAsyncValidator;
|
||||||
|
@ -15,52 +15,12 @@ const AsyncValidator: any = RawAsyncValidator;
|
||||||
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
|
* `I'm ${name}` + { name: 'bamboo' } = I'm bamboo
|
||||||
*/
|
*/
|
||||||
function replaceMessage(template: string, kv: Record<string, string>): string {
|
function replaceMessage(template: string, kv: Record<string, string>): string {
|
||||||
return template.replace(/\$\{\w+\}/g, str => {
|
return template.replace(/\$\{\w+\}/g, (str: string) => {
|
||||||
const key = str.slice(2, -1);
|
const key = str.slice(2, -1);
|
||||||
return kv[key];
|
return kv[key];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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<string, string>,
|
|
||||||
): ValidateMessages {
|
|
||||||
const kv = {
|
|
||||||
...(rule as Record<string, string | number>),
|
|
||||||
name,
|
|
||||||
enum: (rule.enum || []).join(', '),
|
|
||||||
};
|
|
||||||
|
|
||||||
const replaceFunc = (template: string, additionalKV?: Record<string, string>) => () =>
|
|
||||||
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(
|
async function validateRule(
|
||||||
name: string,
|
name: string,
|
||||||
value: any,
|
value: any,
|
||||||
|
@ -69,29 +29,22 @@ async function validateRule(
|
||||||
messageVariables?: Record<string, string>,
|
messageVariables?: Record<string, string>,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const cloneRule = { ...rule };
|
const cloneRule = { ...rule };
|
||||||
|
|
||||||
|
// Bug of `async-validator`
|
||||||
|
delete (cloneRule as any).ruleIndex;
|
||||||
|
|
||||||
// We should special handle array validate
|
// We should special handle array validate
|
||||||
let subRuleField: RuleObject = null;
|
let subRuleField: RuleObject = null;
|
||||||
if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
|
if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) {
|
||||||
subRuleField = cloneRule.defaultField;
|
subRuleField = cloneRule.defaultField;
|
||||||
delete cloneRule.defaultField;
|
delete cloneRule.defaultField;
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
!rule.type &&
|
|
||||||
typeof rule.validator !== 'function' &&
|
|
||||||
typeof value !== 'string' &&
|
|
||||||
typeof value !== 'undefined'
|
|
||||||
) {
|
|
||||||
warning(
|
|
||||||
false,
|
|
||||||
`Form rules must provide type property when validating the form item named [${name}] which is not string type`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const validator = new AsyncValidator({
|
const validator = new AsyncValidator({
|
||||||
[name]: [cloneRule],
|
[name]: [cloneRule],
|
||||||
});
|
});
|
||||||
|
|
||||||
const messages = convertMessages(options.validateMessages, name, cloneRule, messageVariables);
|
const messages = setValues({}, defaultValidateMessages, options.validateMessages);
|
||||||
validator.messages(messages);
|
validator.messages(messages);
|
||||||
|
|
||||||
let result = [];
|
let result = [];
|
||||||
|
@ -120,7 +73,22 @@ async function validateRule(
|
||||||
return subResults.reduce((prev, errors) => [...prev, ...errors], []);
|
return subResults.reduce((prev, errors) => [...prev, ...errors], []);
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
// Replace message with variables
|
||||||
|
const kv = {
|
||||||
|
...(rule as Record<string, string | number>),
|
||||||
|
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 +106,84 @@ export function validateRules(
|
||||||
const name = namePath.join('.');
|
const name = namePath.join('.');
|
||||||
|
|
||||||
// Fill rule with context
|
// Fill rule with context
|
||||||
const filledRules: RuleObject[] = rules.map(currentRule => {
|
const filledRules: RuleObject[] = rules
|
||||||
const originValidatorFunc = currentRule.validator;
|
.map((currentRule, ruleIndex) => {
|
||||||
|
const originValidatorFunc = currentRule.validator;
|
||||||
|
const cloneRule = {
|
||||||
|
...currentRule,
|
||||||
|
ruleIndex,
|
||||||
|
};
|
||||||
|
|
||||||
if (!originValidatorFunc) {
|
// Replace validator if needed
|
||||||
return currentRule;
|
if (originValidatorFunc) {
|
||||||
}
|
cloneRule.validator = (rule: RuleObject, val: any, callback: (error?: string) => void) => {
|
||||||
return {
|
let hasPromise = false;
|
||||||
...currentRule,
|
|
||||||
validator(rule: RuleObject, val: any, callback: (error?: string) => void) {
|
|
||||||
let hasPromise = false;
|
|
||||||
|
|
||||||
// Wrap callback only accept when promise not provided
|
// Wrap callback only accept when promise not provided
|
||||||
const wrappedCallback = (...args: string[]) => {
|
const wrappedCallback = (...args: string[]) => {
|
||||||
// Wait a tick to make sure return type is a promise
|
// Wait a tick to make sure return type is a promise
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
warning(
|
warning(
|
||||||
!hasPromise,
|
!hasPromise,
|
||||||
'Your validator function has already return a promise. `callback` will be ignored.',
|
'Your validator function has already return a promise. `callback` will be ignored.',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasPromise) {
|
if (!hasPromise) {
|
||||||
callback(...args);
|
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<void>)
|
|
||||||
.then(() => {
|
|
||||||
callback();
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
callback(err);
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let summaryPromise: Promise<string[]>;
|
// 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<void>)
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (w1) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do validate rules
|
||||||
|
let summaryPromise: Promise<RuleError[]>;
|
||||||
|
|
||||||
if (validateFirst === true) {
|
if (validateFirst === true) {
|
||||||
// >>>>> Validate by serialization
|
// >>>>> Validate by serialization
|
||||||
summaryPromise = new Promise(async resolve => {
|
summaryPromise = new Promise(async (resolve, reject) => {
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
for (let i = 0; i < filledRules.length; i += 1) {
|
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) {
|
if (errors.length) {
|
||||||
resolve(errors);
|
reject([{ errors, rule }]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,18 +193,15 @@ export function validateRules(
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// >>>>> Validate by parallel
|
// >>>>> Validate by parallel
|
||||||
const rulePromises = filledRules.map(rule =>
|
const rulePromises: Promise<RuleError>[] = filledRules.map(rule =>
|
||||||
validateRule(name, value, rule, options, messageVariables),
|
validateRule(name, value, rule, options, messageVariables).then(errors => ({ errors, rule })),
|
||||||
);
|
);
|
||||||
|
|
||||||
summaryPromise = (
|
summaryPromise = (
|
||||||
validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)
|
validateFirst ? finishOnFirstFailed(rulePromises) : finishOnAllFailed(rulePromises)
|
||||||
).then((errors: string[]): string[] | Promise<string[]> => {
|
).then((errors: RuleError[]): RuleError[] | Promise<RuleError[]> => {
|
||||||
if (!errors.length) {
|
// Always change to rejection for Field to catch
|
||||||
return [];
|
return Promise.reject<RuleError[]>(errors);
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.reject<string[]>(errors);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -228,22 +211,24 @@ export function validateRules(
|
||||||
return summaryPromise;
|
return summaryPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function finishOnAllFailed(rulePromises: Promise<string[]>[]): Promise<string[]> {
|
async function finishOnAllFailed(rulePromises: Promise<RuleError>[]): Promise<RuleError[]> {
|
||||||
return Promise.all(rulePromises).then(errorsList => {
|
return Promise.all(rulePromises).then(
|
||||||
const errors = [].concat(...errorsList);
|
(errorsList: RuleError[]): RuleError[] | Promise<RuleError[]> => {
|
||||||
|
const errors: RuleError[] = [].concat(...errorsList);
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function finishOnFirstFailed(rulePromises: Promise<string[]>[]): Promise<string[]> {
|
async function finishOnFirstFailed(rulePromises: Promise<RuleError>[]): Promise<RuleError[]> {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
|
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
rulePromises.forEach(promise => {
|
rulePromises.forEach(promise => {
|
||||||
promise.then(errors => {
|
promise.then(ruleError => {
|
||||||
if (errors.length) {
|
if (ruleError.errors.length) {
|
||||||
resolve(errors);
|
resolve([ruleError]);
|
||||||
}
|
}
|
||||||
|
|
||||||
count += 1;
|
count += 1;
|
||||||
|
|
Loading…
Reference in New Issue