fix: update form validate
							parent
							
								
									346ab47eb6
								
							
						
					
					
						commit
						f3a9b7b616
					
				|  | @ -1 +1 @@ | |||
| Subproject commit 26019de237561b0d68e22a4e46537ee8e2e6f0fe | ||||
| Subproject commit f836704c60bf7e647e9e59f4a136024a960698b3 | ||||
|  | @ -8,7 +8,10 @@ import warning from '../_util/warning'; | |||
| import FormItem from './FormItem'; | ||||
| import { initDefaultProps, getSlot } from '../_util/props-util'; | ||||
| import { ConfigConsumerProps } from '../config-provider'; | ||||
| import { getParams } from './utils'; | ||||
| import { getNamePath, containsNamePath } from './utils/valueUtil'; | ||||
| import { defaultValidateMessages } from './utils/messages'; | ||||
| import { allPromiseFinish } from './utils/asyncUtil'; | ||||
| import { toArray } from './utils/typeUtil'; | ||||
| 
 | ||||
| export const FormProps = { | ||||
|   layout: PropTypes.oneOf(['horizontal', 'inline', 'vertical']), | ||||
|  | @ -66,6 +69,7 @@ const Form = { | |||
|   created() { | ||||
|     this.fields = []; | ||||
|     this.form = undefined; | ||||
|     this.lastValidatePromise = null; | ||||
|     provide('FormContext', this); | ||||
|   }, | ||||
|   setup() { | ||||
|  | @ -100,12 +104,16 @@ const Form = { | |||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|       this.$emit('submit', e); | ||||
|       const res = this.validate(); | ||||
|       const res = this.validateFields(); | ||||
|       res | ||||
|         .then(values => { | ||||
|           // eslint-disable-next-line no-console | ||||
|           console.log('values', values); | ||||
|           this.$emit('finish', values); | ||||
|         }) | ||||
|         .catch(errors => { | ||||
|           // eslint-disable-next-line no-console | ||||
|           console.log('errors', errors); | ||||
|           this.handleFinishFailed(errors); | ||||
|         }); | ||||
|     }, | ||||
|  | @ -179,74 +187,98 @@ const Form = { | |||
|       // } | ||||
|     }, | ||||
|     scrollToField() {}, | ||||
|     getFieldsValue(allFields) { | ||||
|     // TODO | ||||
|     // eslint-disable-next-line no-unused-vars | ||||
|     getFieldsValue(nameList) { | ||||
|       const values = {}; | ||||
|       allFields.forEach(({ prop, fieldValue }) => { | ||||
|       this.fields.forEach(({ prop, fieldValue }) => { | ||||
|         values[prop] = fieldValue; | ||||
|       }); | ||||
|       return values; | ||||
|     }, | ||||
|     validateFields() { | ||||
|       return this.validateField(...arguments); | ||||
|     }, | ||||
|     validateField(ns, opt, cb) { | ||||
|       const pending = new Promise((resolve, reject) => { | ||||
|         const params = getParams(ns, opt, cb); | ||||
|         const { names, options } = params; | ||||
|         let { callback } = params; | ||||
|         if (!callback || typeof callback === 'function') { | ||||
|           const oldCb = callback; | ||||
|           callback = (errorFields, values) => { | ||||
|             if (oldCb) { | ||||
|               oldCb(errorFields, values); | ||||
|             } else if (errorFields) { | ||||
|               reject({ errorFields, values }); | ||||
|             } else { | ||||
|               resolve(values); | ||||
|             } | ||||
|           }; | ||||
|     validateFields(nameList, options) { | ||||
|       if (!this.model) { | ||||
|         warning(false, 'Form', 'model is required for validateFields to work.'); | ||||
|         return; | ||||
|       } | ||||
|       const provideNameList = !!nameList; | ||||
|       const namePathList = provideNameList ? toArray(nameList).map(getNamePath) : []; | ||||
| 
 | ||||
|       // Collect result in promise list | ||||
|       const promiseList = []; | ||||
| 
 | ||||
|       this.fields.forEach(field => { | ||||
|         // Add field if not provide `nameList` | ||||
|         if (!provideNameList) { | ||||
|           namePathList.push(field.getNamePath()); | ||||
|         } | ||||
|         const allFields = names | ||||
|           ? this.fields.filter(field => names.indexOf(field.prop) !== -1) | ||||
|           : this.fields; | ||||
|         const fields = allFields.filter(field => { | ||||
|           const rules = field.getFilteredRule(''); | ||||
|           return rules && rules.length; | ||||
|         }); | ||||
|         if (!fields.length) { | ||||
|           callback(null, this.getFieldsValue(allFields)); | ||||
| 
 | ||||
|         // Skip if without rule | ||||
|         if (!field.getRules().length) { | ||||
|           return; | ||||
|         } | ||||
|         if (!('firstFields' in options)) { | ||||
|           options.firstFields = allFields.filter(field => { | ||||
|             return !!field.validateFirst; | ||||
|           }); | ||||
|         } | ||||
|         let fieldsErrors = {}; | ||||
|         let valid = true; | ||||
|         let count = 0; | ||||
|         const promiseList = []; | ||||
|         fields.forEach(field => { | ||||
|           const promise = field.validate('', errors => { | ||||
|             if (errors) { | ||||
|               valid = false; | ||||
|               fieldsErrors[field.prop] = errors; | ||||
|             } | ||||
| 
 | ||||
|             if (++count === fields.length) { | ||||
|               callback(valid ? null : fieldsErrors, this.getFieldsValue(fields)); | ||||
|             } | ||||
|         const fieldNamePath = field.getNamePath(); | ||||
| 
 | ||||
|         // Add field validate rule in to promise list | ||||
|         if (!provideNameList || containsNamePath(namePathList, fieldNamePath)) { | ||||
|           const promise = field.validateRules({ | ||||
|             validateMessages: { | ||||
|               ...defaultValidateMessages, | ||||
|               ...this.validateMessages, | ||||
|             }, | ||||
|             ...options, | ||||
|           }); | ||||
|           promiseList.push(promise.then(() => {})); | ||||
|         }); | ||||
|       }); | ||||
|       pending.catch(e => { | ||||
|         if (console.error && process.env.NODE_ENV !== 'production') { | ||||
|           console.error(e); | ||||
| 
 | ||||
|           // Wrap promise with field | ||||
|           promiseList.push( | ||||
|             promise | ||||
|               .then(() => ({ name: fieldNamePath, errors: [] })) | ||||
|               .catch(errors => | ||||
|                 Promise.reject({ | ||||
|                   name: fieldNamePath, | ||||
|                   errors, | ||||
|                 }), | ||||
|               ), | ||||
|           ); | ||||
|         } | ||||
|         return e; | ||||
|       }); | ||||
|       return pending; | ||||
| 
 | ||||
|       const summaryPromise = allPromiseFinish(promiseList); | ||||
|       this.lastValidatePromise = summaryPromise; | ||||
| 
 | ||||
|       // // Notify fields with rule that validate has finished and need update | ||||
|       // summaryPromise | ||||
|       //   .catch(results => results) | ||||
|       //   .then(results => { | ||||
|       //     const resultNamePathList = results.map(({ name }) => name); | ||||
|       //     // eslint-disable-next-line no-console | ||||
|       //     console.log(resultNamePathList); | ||||
|       //   }); | ||||
| 
 | ||||
|       const returnPromise = summaryPromise | ||||
|         .then(() => { | ||||
|           if (this.lastValidatePromise === summaryPromise) { | ||||
|             return Promise.resolve(this.getFieldsValue(namePathList)); | ||||
|           } | ||||
|           return Promise.reject([]); | ||||
|         }) | ||||
|         .catch(results => { | ||||
|           const errorList = results.filter(result => result && result.errors.length); | ||||
|           return Promise.reject({ | ||||
|             values: this.getFieldsValue(namePathList), | ||||
|             errorFields: errorList, | ||||
|             outOfDate: this.lastValidatePromise !== summaryPromise, | ||||
|           }); | ||||
|         }); | ||||
| 
 | ||||
|       // Do not throw in console | ||||
|       returnPromise.catch(e => e); | ||||
| 
 | ||||
|       return returnPromise; | ||||
|     }, | ||||
|     validateField() { | ||||
|       return this.validateFields(...arguments); | ||||
|     }, | ||||
|   }, | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import { inject, provide, Transition } from 'vue'; | ||||
| import AsyncValidator from 'async-validator'; | ||||
| import cloneDeep from 'lodash/cloneDeep'; | ||||
| import PropTypes from '../_util/vue-types'; | ||||
| import classNames from 'classnames'; | ||||
|  | @ -22,8 +21,9 @@ import CheckCircleFilled from '@ant-design/icons-vue/CheckCircleFilled'; | |||
| import ExclamationCircleFilled from '@ant-design/icons-vue/ExclamationCircleFilled'; | ||||
| import CloseCircleFilled from '@ant-design/icons-vue/CloseCircleFilled'; | ||||
| import LoadingOutlined from '@ant-design/icons-vue/LoadingOutlined'; | ||||
| import { finishOnAllFailed, finishOnFirstFailed, getNamePath } from './utils'; | ||||
| import { warning } from '../vc-util/warning'; | ||||
| import { validateRules } from './utils/validateUtil'; | ||||
| import { getNamePath } from './utils/valueUtil'; | ||||
| import { toArray } from './utils/typeUtil'; | ||||
| 
 | ||||
| const iconMap = { | ||||
|   success: CheckCircleFilled, | ||||
|  | @ -100,6 +100,7 @@ export default { | |||
|       validateDisabled: false, | ||||
|       validator: {}, | ||||
|       helpShow: false, | ||||
|       errors: [], | ||||
|     }; | ||||
|   }, | ||||
| 
 | ||||
|  | @ -155,188 +156,51 @@ export default { | |||
|     removeField && removeField(this); | ||||
|   }, | ||||
|   methods: { | ||||
|     async validateRule(name, value, rule) { | ||||
|       const cloneRule = { ...rule }; | ||||
|       // We should special handle array validate | ||||
|       let subRuleField = null; | ||||
|       if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) { | ||||
|         subRuleField = cloneRule.defaultField; | ||||
|         delete cloneRule.defaultField; | ||||
|       } | ||||
|       let result = []; | ||||
|       const validator = new AsyncValidator({ | ||||
|         [name]: [cloneRule], | ||||
|       }); | ||||
|       if (this.FormContext && this.FormContext.validateMessages) { | ||||
|         validator.messages(this.FormContext.validateMessages); | ||||
|       } | ||||
|       try { | ||||
|         await validator.validate( | ||||
|           { [this.prop]: this.fieldValue }, | ||||
|           { firstFields: !!this.validateFirst }, | ||||
|         ); | ||||
|       } catch (errObj) { | ||||
|         if (errObj.errors) { | ||||
|           result = errObj.errors.map(({ message }) => message); | ||||
|         } else { | ||||
|           console.error(errObj); | ||||
|         } | ||||
|       } | ||||
|       if (!result.length && subRuleField) { | ||||
|         const subResults = await Promise.all( | ||||
|           value.map((subValue, i) => this.validateRule(`${name}.${i}`, subValue, subRuleField)), | ||||
|         ); | ||||
|     getNamePath() { | ||||
|       const { prop } = this.$props; | ||||
|       const { prefixName = [] } = this.FormContext; | ||||
| 
 | ||||
|         return subResults.reduce((prev, errors) => [...prev, ...errors], []); | ||||
|       } | ||||
|       return result; | ||||
|       return prop !== undefined ? [...prefixName, ...getNamePath(prop)] : []; | ||||
|     }, | ||||
|     validateRules(namePath, value, rules, validateFirst) { | ||||
|       const name = namePath.join('.'); | ||||
|     validateRules(options) { | ||||
|       const { validateFirst = false, messageVariables } = this.$props; | ||||
|       const { triggerName } = options || {}; | ||||
|       const namePath = this.getNamePath(); | ||||
| 
 | ||||
|       // Fill rule with context | ||||
|       const filledRules = rules.map(currentRule => { | ||||
|         const originValidatorFunc = currentRule.validator; | ||||
| 
 | ||||
|         if (!originValidatorFunc) { | ||||
|           return currentRule; | ||||
|         } | ||||
|         return { | ||||
|           ...currentRule, | ||||
|           validator(rule, val, callback) { | ||||
|             let hasPromise = false; | ||||
| 
 | ||||
|             // Wrap callback only accept when promise not provided | ||||
|             const wrappedCallback = (...args) => { | ||||
|               // 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 | ||||
|                 .then(() => { | ||||
|                   callback(); | ||||
|                 }) | ||||
|                 .catch(err => { | ||||
|                   callback(err); | ||||
|                 }); | ||||
|             } | ||||
|           }, | ||||
|         }; | ||||
|       }); | ||||
| 
 | ||||
|       let summaryPromise; | ||||
| 
 | ||||
|       if (validateFirst === true) { | ||||
|         // >>>>> Validate by serialization | ||||
|         summaryPromise = new Promise(async resolve => { | ||||
|           /* eslint-disable no-await-in-loop */ | ||||
|           for (let i = 0; i < filledRules.length; i += 1) { | ||||
|             const errors = await this.validateRule(name, value, filledRules[i]); | ||||
|             if (errors.length) { | ||||
|               resolve(errors); | ||||
|               return; | ||||
|             } | ||||
|       let filteredRules = this.getRules(); | ||||
|       if (triggerName) { | ||||
|         filteredRules = filteredRules.filter(rule => { | ||||
|           const { validateTrigger } = rule; | ||||
|           if (!validateTrigger) { | ||||
|             return true; | ||||
|           } | ||||
|           /* eslint-enable */ | ||||
| 
 | ||||
|           resolve([]); | ||||
|         }); | ||||
|       } else { | ||||
|         // >>>>> Validate by parallel | ||||
|         const rulePromises = filledRules.map(rule => this.validateRule(name, value, rule)); | ||||
| 
 | ||||
|         summaryPromise = (validateFirst | ||||
|           ? finishOnFirstFailed(rulePromises) | ||||
|           : finishOnAllFailed(rulePromises) | ||||
|         ).then(errors => { | ||||
|           if (!errors.length) { | ||||
|             return []; | ||||
|           } | ||||
| 
 | ||||
|           return Promise.reject(errors); | ||||
|           const triggerList = toArray(validateTrigger); | ||||
|           return triggerList.includes(triggerName); | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       // Internal catch error to avoid console error log. | ||||
|       summaryPromise.catch(e => e); | ||||
| 
 | ||||
|       return summaryPromise; | ||||
|     }, | ||||
|     validate(trigger) { | ||||
|       this.validateDisabled = false; | ||||
|       const rules = this.getFilteredRule(trigger); | ||||
|       if (!rules || rules.length === 0) { | ||||
|         return; | ||||
|       } | ||||
|       const promise = validateRules( | ||||
|         namePath, | ||||
|         this.fieldValue, | ||||
|         filteredRules, | ||||
|         options, | ||||
|         validateFirst, | ||||
|         messageVariables, | ||||
|       ); | ||||
|       this.validateState = 'validating'; | ||||
|       if (rules && rules.length > 0) { | ||||
|         rules.forEach(rule => { | ||||
|           delete rule.trigger; | ||||
|         }); | ||||
|       } | ||||
|       // descriptor[this.prop] = rules; | ||||
|       // const validator = new AsyncValidator(descriptor); | ||||
|       // if (this.FormContext && this.FormContext.validateMessages) { | ||||
|       //   validator.messages(this.FormContext.validateMessages); | ||||
|       // } | ||||
|       const fieldNamePath = getNamePath(this.prop); | ||||
|       // const promiseList = []; | ||||
|       const promise = this.validateRules(fieldNamePath, this.fieldValue, rules, this.validateFirst); | ||||
|       this.errors = []; | ||||
| 
 | ||||
|       promise | ||||
|         .then(res => { | ||||
|           // eslint-disable-next-line no-console | ||||
|           console.log(res); | ||||
|           this.validateState = 'success'; | ||||
|           this.validateMessage = ''; | ||||
|           return { name: fieldNamePath, errors: [] }; | ||||
|         }) | ||||
|         .catch(errors => { | ||||
|           this.validateState = 'error'; | ||||
|           this.validateMessage = errors; | ||||
|           Promise.reject({ | ||||
|             name: fieldNamePath, | ||||
|             errors, | ||||
|           }); | ||||
|         .catch(e => e) | ||||
|         .then((errors = []) => { | ||||
|           if (this.validateState === 'validating') { | ||||
|             this.validateState = errors.length ? 'error' : 'success'; | ||||
|             this.validateMessage = errors[0]; | ||||
|             this.errors = errors; | ||||
|           } | ||||
|         }); | ||||
| 
 | ||||
|       return promise; | ||||
|       // // Wrap promise with field | ||||
|       // promiseList.push( | ||||
|       //   promise | ||||
|       //     .then(() => ({ name: fieldNamePath, errors: [] })) | ||||
|       //     .catch(errors => | ||||
|       //       Promise.reject({ | ||||
|       //         name: fieldNamePath, | ||||
|       //         errors, | ||||
|       //       }), | ||||
|       //     ), | ||||
|       // ); | ||||
|       // this.validateState = result.length ? 'error' : 'success'; | ||||
|       // this.validateMessage = result.length ? result[0] : ''; | ||||
|       // this.FormContext && | ||||
|       //   this.FormContext.$emit && | ||||
|       //   this.FormContext.$emit('validate', this.prop, !result.length, this.validateMessage || null); | ||||
|       // return result; | ||||
|     }, | ||||
|     getRules() { | ||||
|       let formRules = this.FormContext.rules; | ||||
|  | @ -361,14 +225,14 @@ export default { | |||
|         .map(rule => ({ ...rule })); | ||||
|     }, | ||||
|     onFieldBlur() { | ||||
|       this.validate('blur'); | ||||
|       this.validateRules({ triggerName: 'blur' }); | ||||
|     }, | ||||
|     onFieldChange() { | ||||
|       if (this.validateDisabled) { | ||||
|         this.validateDisabled = false; | ||||
|         return; | ||||
|       } | ||||
|       this.validate('change'); | ||||
|       this.validateRules({ triggerName: 'change' }); | ||||
|     }, | ||||
|     clearValidate() { | ||||
|       this.validateState = ''; | ||||
|  |  | |||
|  | @ -103,41 +103,41 @@ export function getScrollableContainer(n) { | |||
|   return nodeName === 'body' ? node.ownerDocument : node; | ||||
| } | ||||
| 
 | ||||
| export async function finishOnAllFailed(rulePromises) { | ||||
|   return Promise.all(rulePromises).then(errorsList => { | ||||
|     const errors = [].concat(...errorsList); | ||||
| // export async function finishOnAllFailed(rulePromises) {
 | ||||
| //   return Promise.all(rulePromises).then(errorsList => {
 | ||||
| //     const errors = [].concat(...errorsList);
 | ||||
| 
 | ||||
|     return errors; | ||||
|   }); | ||||
| } | ||||
| //     return errors;
 | ||||
| //   });
 | ||||
| // }
 | ||||
| 
 | ||||
| export async function finishOnFirstFailed(rulePromises) { | ||||
|   let count = 0; | ||||
| // export async function finishOnFirstFailed(rulePromises) {
 | ||||
| //   let count = 0;
 | ||||
| 
 | ||||
|   return new Promise(resolve => { | ||||
|     rulePromises.forEach(promise => { | ||||
|       promise.then(errors => { | ||||
|         if (errors.length) { | ||||
|           resolve(errors); | ||||
|         } | ||||
| //   return new Promise(resolve => {
 | ||||
| //     rulePromises.forEach(promise => {
 | ||||
| //       promise.then(errors => {
 | ||||
| //         if (errors.length) {
 | ||||
| //           resolve(errors);
 | ||||
| //         }
 | ||||
| 
 | ||||
|         count += 1; | ||||
|         if (count === rulePromises.length) { | ||||
|           resolve([]); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
| //         count += 1;
 | ||||
| //         if (count === rulePromises.length) {
 | ||||
| //           resolve([]);
 | ||||
| //         }
 | ||||
| //       });
 | ||||
| //     });
 | ||||
| //   });
 | ||||
| // }
 | ||||
| 
 | ||||
| export function toArray(value) { | ||||
|   if (value === undefined || value === null) { | ||||
|     return []; | ||||
|   } | ||||
| // export function toArray(value) {
 | ||||
| //   if (value === undefined || value === null) {
 | ||||
| //     return [];
 | ||||
| //   }
 | ||||
| 
 | ||||
|   return Array.isArray(value) ? value : [value]; | ||||
| } | ||||
| //   return Array.isArray(value) ? value : [value];
 | ||||
| // }
 | ||||
| 
 | ||||
| export function getNamePath(path) { | ||||
|   return toArray(path); | ||||
| } | ||||
| // export function getNamePath(path) {
 | ||||
| //   return toArray(path);
 | ||||
| // }
 | ||||
|  |  | |||
|  | @ -0,0 +1,32 @@ | |||
| export function allPromiseFinish(promiseList) { | ||||
|   let hasError = false; | ||||
|   let count = promiseList.length; | ||||
|   const results = []; | ||||
| 
 | ||||
|   if (!promiseList.length) { | ||||
|     return Promise.resolve([]); | ||||
|   } | ||||
| 
 | ||||
|   return new Promise((resolve, reject) => { | ||||
|     promiseList.forEach((promise, index) => { | ||||
|       promise | ||||
|         .catch(e => { | ||||
|           hasError = true; | ||||
|           return e; | ||||
|         }) | ||||
|         .then(result => { | ||||
|           count -= 1; | ||||
|           results[index] = result; | ||||
| 
 | ||||
|           if (count > 0) { | ||||
|             return; | ||||
|           } | ||||
| 
 | ||||
|           if (hasError) { | ||||
|             reject(results); | ||||
|           } | ||||
|           resolve(results); | ||||
|         }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | @ -0,0 +1,7 @@ | |||
| export function toArray(value) { | ||||
|   if (value === undefined || value === null) { | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   return Array.isArray(value) ? value : [value]; | ||||
| } | ||||
|  | @ -0,0 +1,227 @@ | |||
| import RawAsyncValidator from 'async-validator'; | ||||
| import { cloneVNode } from 'vue'; | ||||
| import { warning } from '../../vc-util/warning'; | ||||
| 
 | ||||
| import { setValues } from './valueUtil'; | ||||
| import { defaultValidateMessages } from './messages'; | ||||
| import { isValidElement } from '../../_util/props-util'; | ||||
| 
 | ||||
| // Remove incorrect original ts define
 | ||||
| const AsyncValidator = RawAsyncValidator; | ||||
| 
 | ||||
| /** | ||||
|  * Replace with template. | ||||
|  *   `I'm ${name}` + { name: 'bamboo' } = I'm bamboo | ||||
|  */ | ||||
| function replaceMessage(template, kv) { | ||||
|   return template.replace(/\$\{\w+\}/g, str => { | ||||
|     const key = str.slice(2, -1); | ||||
|     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, name, rule, messageVariables) { | ||||
|   const kv = { | ||||
|     ...rule, | ||||
|     name, | ||||
|     enum: (rule.enum || []).join(', '), | ||||
|   }; | ||||
| 
 | ||||
|   const replaceFunc = (template, additionalKV) => () => | ||||
|     replaceMessage(template, { ...kv, ...additionalKV }); | ||||
| 
 | ||||
|   /* eslint-disable no-param-reassign */ | ||||
|   function fillTemplate(source, target = {}) { | ||||
|     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)); | ||||
| } | ||||
| 
 | ||||
| async function validateRule(name, value, rule, options, messageVariables) { | ||||
|   const cloneRule = { ...rule }; | ||||
|   // We should special handle array validate
 | ||||
|   let subRuleField = null; | ||||
|   if (cloneRule && cloneRule.type === 'array' && cloneRule.defaultField) { | ||||
|     subRuleField = cloneRule.defaultField; | ||||
|     delete cloneRule.defaultField; | ||||
|   } | ||||
| 
 | ||||
|   const validator = new AsyncValidator({ | ||||
|     [name]: [cloneRule], | ||||
|   }); | ||||
| 
 | ||||
|   const messages = convertMessages(options.validateMessages, name, cloneRule, messageVariables); | ||||
|   validator.messages(messages); | ||||
| 
 | ||||
|   let result = []; | ||||
| 
 | ||||
|   try { | ||||
|     await Promise.resolve(validator.validate({ [name]: value }, { ...options })); | ||||
|   } catch (errObj) { | ||||
|     if (errObj.errors) { | ||||
|       result = errObj.errors.map(({ message }, index) => | ||||
|         // Wrap VueNode with `key`
 | ||||
|         isValidElement(message) ? cloneVNode(message, { key: `error_${index}` }) : message, | ||||
|       ); | ||||
|     } else { | ||||
|       console.error(errObj); | ||||
|       result = [messages.default()]; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   if (!result.length && subRuleField) { | ||||
|     const subResults = await Promise.all( | ||||
|       value.map((subValue, i) => | ||||
|         validateRule(`${name}.${i}`, subValue, subRuleField, options, messageVariables), | ||||
|       ), | ||||
|     ); | ||||
| 
 | ||||
|     return subResults.reduce((prev, errors) => [...prev, ...errors], []); | ||||
|   } | ||||
| 
 | ||||
|   return result; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * We use `async-validator` to validate the value. | ||||
|  * But only check one value in a time to avoid namePath validate issue. | ||||
|  */ | ||||
| export function validateRules(namePath, value, rules, options, validateFirst, messageVariables) { | ||||
|   const name = namePath.join('.'); | ||||
| 
 | ||||
|   // Fill rule with context
 | ||||
|   const filledRules = rules.map(currentRule => { | ||||
|     const originValidatorFunc = currentRule.validator; | ||||
| 
 | ||||
|     if (!originValidatorFunc) { | ||||
|       return currentRule; | ||||
|     } | ||||
|     return { | ||||
|       ...currentRule, | ||||
|       validator(rule, val, callback) { | ||||
|         let hasPromise = false; | ||||
| 
 | ||||
|         // Wrap callback only accept when promise not provided
 | ||||
|         const wrappedCallback = (...args) => { | ||||
|           // 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 | ||||
|             .then(() => { | ||||
|               callback(); | ||||
|             }) | ||||
|             .catch(err => { | ||||
|               callback(err); | ||||
|             }); | ||||
|         } | ||||
|       }, | ||||
|     }; | ||||
|   }); | ||||
| 
 | ||||
|   let summaryPromise; | ||||
| 
 | ||||
|   if (validateFirst === true) { | ||||
|     // >>>>> Validate by serialization
 | ||||
|     summaryPromise = new Promise(async resolve => { | ||||
|       /* 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); | ||||
|         if (errors.length) { | ||||
|           resolve(errors); | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|       /* eslint-enable */ | ||||
| 
 | ||||
|       resolve([]); | ||||
|     }); | ||||
|   } else { | ||||
|     // >>>>> Validate by parallel
 | ||||
|     const rulePromises = filledRules.map(rule => | ||||
|       validateRule(name, value, rule, options, messageVariables), | ||||
|     ); | ||||
| 
 | ||||
|     summaryPromise = (validateFirst | ||||
|       ? finishOnFirstFailed(rulePromises) | ||||
|       : finishOnAllFailed(rulePromises) | ||||
|     ).then(errors => { | ||||
|       if (!errors.length) { | ||||
|         return []; | ||||
|       } | ||||
| 
 | ||||
|       return Promise.reject(errors); | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   // Internal catch error to avoid console error log.
 | ||||
|   summaryPromise.catch(e => e); | ||||
| 
 | ||||
|   return summaryPromise; | ||||
| } | ||||
| 
 | ||||
| async function finishOnAllFailed(rulePromises) { | ||||
|   return Promise.all(rulePromises).then(errorsList => { | ||||
|     const errors = [].concat(...errorsList); | ||||
| 
 | ||||
|     return errors; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| async function finishOnFirstFailed(rulePromises) { | ||||
|   let count = 0; | ||||
| 
 | ||||
|   return new Promise(resolve => { | ||||
|     rulePromises.forEach(promise => { | ||||
|       promise.then(errors => { | ||||
|         if (errors.length) { | ||||
|           resolve(errors); | ||||
|         } | ||||
| 
 | ||||
|         count += 1; | ||||
|         if (count === rulePromises.length) { | ||||
|           resolve([]); | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| } | ||||
|  | @ -0,0 +1,74 @@ | |||
| import { toArray } from './typeUtil'; | ||||
| 
 | ||||
| /** | ||||
|  * Convert name to internal supported format. | ||||
|  * This function should keep since we still thinking if need support like `a.b.c` format. | ||||
|  * 'a' => ['a'] | ||||
|  * 123 => [123] | ||||
|  * ['a', 123] => ['a', 123] | ||||
|  */ | ||||
| export function getNamePath(path) { | ||||
|   return toArray(path); | ||||
| } | ||||
| 
 | ||||
| // export function getValue(store: Store, namePath: InternalNamePath) {
 | ||||
| //   const value = get(store, namePath);
 | ||||
| //   return value;
 | ||||
| // }
 | ||||
| 
 | ||||
| // export function setValue(store: Store, namePath: InternalNamePath, value: StoreValue): Store {
 | ||||
| //   const newStore = set(store, namePath, value);
 | ||||
| //   return newStore;
 | ||||
| // }
 | ||||
| 
 | ||||
| // export function cloneByNamePathList(store: Store, namePathList: InternalNamePath[]): Store {
 | ||||
| //   let newStore = {};
 | ||||
| //   namePathList.forEach(namePath => {
 | ||||
| //     const value = getValue(store, namePath);
 | ||||
| //     newStore = setValue(newStore, namePath, value);
 | ||||
| //   });
 | ||||
| 
 | ||||
| //   return newStore;
 | ||||
| // }
 | ||||
| 
 | ||||
| export function containsNamePath(namePathList, namePath) { | ||||
|   return namePathList && namePathList.some(path => matchNamePath(path, namePath)); | ||||
| } | ||||
| 
 | ||||
| function isObject(obj) { | ||||
|   return typeof obj === 'object' && obj !== null && Object.getPrototypeOf(obj) === Object.prototype; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Copy values into store and return a new values object | ||||
|  * ({ a: 1, b: { c: 2 } }, { a: 4, b: { d: 5 } }) => { a: 4, b: { c: 2, d: 5 } } | ||||
|  */ | ||||
| function internalSetValues(store, values) { | ||||
|   const newStore = Array.isArray(store) ? [...store] : { ...store }; | ||||
| 
 | ||||
|   if (!values) { | ||||
|     return newStore; | ||||
|   } | ||||
| 
 | ||||
|   Object.keys(values).forEach(key => { | ||||
|     const prevValue = newStore[key]; | ||||
|     const value = values[key]; | ||||
| 
 | ||||
|     // If both are object (but target is not array), we use recursion to set deep value
 | ||||
|     const recursive = isObject(prevValue) && isObject(value); | ||||
|     newStore[key] = recursive ? internalSetValues(prevValue, value || {}) : value; | ||||
|   }); | ||||
| 
 | ||||
|   return newStore; | ||||
| } | ||||
| 
 | ||||
| export function setValues(store, ...restValues) { | ||||
|   return restValues.reduce((current, newStore) => internalSetValues(current, newStore), store); | ||||
| } | ||||
| 
 | ||||
| export function matchNamePath(namePath, changedNamePath) { | ||||
|   if (!namePath || !changedNamePath || namePath.length !== changedNamePath.length) { | ||||
|     return false; | ||||
|   } | ||||
|   return namePath.every((nameUnit, i) => changedNamePath[i] === nameUnit); | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	 tangjinzhou
						tangjinzhou