fix: form validateFirst not reject #4273

pull/4283/head
tangjinzhou 2021-06-28 11:21:23 +08:00
parent 4a0fce5a0a
commit 5f8d8a555b
5 changed files with 155 additions and 134 deletions

View File

@ -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,
), };
}),
); );
} }
}); });

View File

@ -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);
} }
}); });

View File

@ -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;

View File

@ -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([]);

View File

@ -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;