feat: add useForm
							parent
							
								
									b6688c4419
								
							
						
					
					
						commit
						cdbe8eea6b
					
				|  | @ -1,4 +1,5 @@ | |||
| import { BaseTransitionProps, CSSProperties, getCurrentInstance, onUpdated, Ref } from 'vue'; | ||||
| import type { BaseTransitionProps, CSSProperties, Ref } from 'vue'; | ||||
| import { getCurrentInstance, onUpdated } from 'vue'; | ||||
| import { defineComponent, nextTick, Transition as T, TransitionGroup as TG } from 'vue'; | ||||
| 
 | ||||
| export const getTransitionProps = (transitionName: string, opt: object = {}) => { | ||||
|  |  | |||
|  | @ -1,6 +1,7 @@ | |||
| import type { App, Plugin } from 'vue'; | ||||
| import Form, { formProps } from './Form'; | ||||
| import FormItem, { formItemProps } from './FormItem'; | ||||
| import useForm from './useForm'; | ||||
| 
 | ||||
| export type { FormProps } from './Form'; | ||||
| export type { FormItemProps } from './FormItem'; | ||||
|  | @ -12,8 +13,11 @@ Form.install = function (app: App) { | |||
|   return app; | ||||
| }; | ||||
| 
 | ||||
| export { FormItem, formItemProps, formProps }; | ||||
| export { FormItem, formItemProps, formProps, useForm }; | ||||
| 
 | ||||
| Form.useForm = useForm; | ||||
| export default Form as typeof Form & | ||||
|   Plugin & { | ||||
|     readonly Item: typeof Form.Item; | ||||
|     readonly useForm: typeof useForm; | ||||
|   }; | ||||
|  |  | |||
|  | @ -90,7 +90,7 @@ export interface ValidateErrorEntity<Values = any> { | |||
| } | ||||
| 
 | ||||
| export interface FieldError { | ||||
|   name: InternalNamePath; | ||||
|   name: InternalNamePath | string; | ||||
|   errors: string[]; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,360 @@ | |||
| import type { Ref } from 'vue'; | ||||
| import { computed } from 'vue'; | ||||
| import { reactive, watch, nextTick, unref } from 'vue'; | ||||
| import cloneDeep from 'lodash-es/cloneDeep'; | ||||
| import intersection from 'lodash-es/intersection'; | ||||
| import isEqual from 'lodash-es/isEqual'; | ||||
| import debounce from 'lodash-es/debounce'; | ||||
| import omit from 'lodash-es/omit'; | ||||
| import { validateRules } from './utils/validateUtil'; | ||||
| import { defaultValidateMessages } from './utils/messages'; | ||||
| import { allPromiseFinish } from './utils/asyncUtil'; | ||||
| import type { RuleError, ValidateMessages } from './interface'; | ||||
| import type { ValidateStatus } from './FormItem'; | ||||
| 
 | ||||
| interface DebounceSettings { | ||||
|   leading?: boolean; | ||||
| 
 | ||||
|   wait?: number; | ||||
| 
 | ||||
|   trailing?: boolean; | ||||
| } | ||||
| 
 | ||||
| function isRequired(rules: any[]) { | ||||
|   let isRequired = false; | ||||
|   if (rules && rules.length) { | ||||
|     rules.every((rule: { required: any }) => { | ||||
|       if (rule.required) { | ||||
|         isRequired = true; | ||||
|         return false; | ||||
|       } | ||||
|       return true; | ||||
|     }); | ||||
|   } | ||||
|   return isRequired; | ||||
| } | ||||
| 
 | ||||
| function toArray(value: string | string[]) { | ||||
|   if (value === undefined || value === null) { | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   return Array.isArray(value) ? value : [value]; | ||||
| } | ||||
| 
 | ||||
| export interface Props { | ||||
|   [key: string]: any; | ||||
| } | ||||
| 
 | ||||
| export interface validateOptions { | ||||
|   validateFirst?: boolean; | ||||
|   validateMessages?: ValidateMessages; | ||||
|   trigger?: 'change' | 'blur' | string | string[]; | ||||
| } | ||||
| 
 | ||||
| type namesType = string | string[]; | ||||
| export interface ValidateInfo { | ||||
|   autoLink?: boolean; | ||||
|   required?: boolean; | ||||
|   validateStatus?: ValidateStatus; | ||||
|   help?: any; | ||||
| } | ||||
| 
 | ||||
| export interface validateInfos { | ||||
|   [key: string]: ValidateInfo; | ||||
| } | ||||
| 
 | ||||
| function getPropByPath(obj: Props, path: string, strict: boolean) { | ||||
|   let tempObj = obj; | ||||
|   path = path.replace(/\[(\w+)\]/g, '.$1'); | ||||
|   path = path.replace(/^\./, ''); | ||||
| 
 | ||||
|   const keyArr = path.split('.'); | ||||
|   let i = 0; | ||||
|   for (let len = keyArr.length; i < len - 1; ++i) { | ||||
|     if (!tempObj && !strict) break; | ||||
|     const key = keyArr[i]; | ||||
|     if (key in tempObj) { | ||||
|       tempObj = tempObj[key]; | ||||
|     } else { | ||||
|       if (strict) { | ||||
|         throw new Error('please transfer a valid name path to validate!'); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   return { | ||||
|     o: tempObj, | ||||
|     k: keyArr[i], | ||||
|     v: tempObj ? tempObj[keyArr[i]] : null, | ||||
|     isValid: tempObj && keyArr[i] in tempObj, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function useForm( | ||||
|   modelRef: Props | Ref<Props>, | ||||
|   rulesRef?: Props | Ref<Props>, | ||||
|   options?: { | ||||
|     immediate?: boolean; | ||||
|     deep?: boolean; | ||||
|     validateOnRuleChange?: boolean; | ||||
|     debounce?: DebounceSettings; | ||||
|   }, | ||||
| ): { | ||||
|   modelRef: Props | Ref<Props>; | ||||
|   rulesRef: Props | Ref<Props>; | ||||
|   initialModel: Props; | ||||
|   validateInfos: validateInfos; | ||||
|   resetFields: (newValues?: Props) => void; | ||||
|   validate: <T = any>(names?: namesType, option?: validateOptions) => Promise<T>; | ||||
|   validateField: ( | ||||
|     name?: string, | ||||
|     value?: any, | ||||
|     rules?: [Record<string, unknown>], | ||||
|     option?: validateOptions, | ||||
|   ) => Promise<RuleError[]>; | ||||
|   mergeValidateInfo: (items: ValidateInfo | ValidateInfo[]) => ValidateInfo; | ||||
|   clearValidate: (names?: namesType) => void; | ||||
| } { | ||||
|   const initialModel = cloneDeep(unref(modelRef)); | ||||
|   let validateInfos: validateInfos = {}; | ||||
| 
 | ||||
|   const rulesKeys = computed(() => { | ||||
|     return Object.keys(unref(rulesRef)); | ||||
|   }); | ||||
| 
 | ||||
|   rulesKeys.value.forEach(key => { | ||||
|     validateInfos[key] = { | ||||
|       autoLink: false, | ||||
|       required: isRequired(unref(rulesRef)[key]), | ||||
|     }; | ||||
|   }); | ||||
|   validateInfos = reactive(validateInfos); | ||||
|   const resetFields = (newValues: Props) => { | ||||
|     Object.assign(unref(modelRef), { | ||||
|       ...cloneDeep(initialModel), | ||||
|       ...newValues, | ||||
|     }); | ||||
|     nextTick(() => { | ||||
|       Object.keys(validateInfos).forEach(key => { | ||||
|         validateInfos[key] = { | ||||
|           autoLink: false, | ||||
|           required: isRequired(unref(rulesRef)[key]), | ||||
|         }; | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
|   const filterRules = (rules = [], trigger: string[]) => { | ||||
|     if (!trigger.length) { | ||||
|       return rules; | ||||
|     } else { | ||||
|       return rules.filter(rule => { | ||||
|         const triggerList = toArray(rule.trigger || 'change'); | ||||
|         return intersection(triggerList, trigger).length; | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   let lastValidatePromise = null; | ||||
|   const validateFields = (names: string[], option: validateOptions = {}, strict: boolean) => { | ||||
|     // Collect result in promise list
 | ||||
|     const promiseList: Promise<{ | ||||
|       name: string; | ||||
|       errors: string[]; | ||||
|     }>[] = []; | ||||
|     const values = {}; | ||||
|     for (let i = 0; i < names.length; i++) { | ||||
|       const name = names[i]; | ||||
|       const prop = getPropByPath(unref(modelRef), name, strict); | ||||
|       if (!prop.isValid) continue; | ||||
|       values[name] = prop.v; | ||||
|       const rules = filterRules(unref(rulesRef)[name], toArray(option && option.trigger)); | ||||
|       if (rules.length) { | ||||
|         promiseList.push( | ||||
|           validateField(name, prop.v, rules, option || {}) | ||||
|             .then(() => ({ | ||||
|               name, | ||||
|               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, | ||||
|                   errors: mergedErrors, | ||||
|                   warnings: mergedWarnings, | ||||
|                 }); | ||||
|               } | ||||
| 
 | ||||
|               return { | ||||
|                 name, | ||||
|                 errors: mergedErrors, | ||||
|                 warnings: mergedWarnings, | ||||
|               }; | ||||
|             }), | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     const summaryPromise = allPromiseFinish(promiseList); | ||||
|     lastValidatePromise = summaryPromise; | ||||
| 
 | ||||
|     const returnPromise = summaryPromise | ||||
|       .then(() => { | ||||
|         if (lastValidatePromise === summaryPromise) { | ||||
|           return Promise.resolve(values); | ||||
|         } | ||||
|         return Promise.reject([]); | ||||
|       }) | ||||
|       .catch((results: any[]) => { | ||||
|         const errorList = results.filter( | ||||
|           (result: { errors: string | any[] }) => result && result.errors.length, | ||||
|         ); | ||||
|         return Promise.reject({ | ||||
|           values, | ||||
|           errorFields: errorList, | ||||
|           outOfDate: lastValidatePromise !== summaryPromise, | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|     // Do not throw in console
 | ||||
|     returnPromise.catch((e: any) => e); | ||||
| 
 | ||||
|     return returnPromise; | ||||
|   }; | ||||
|   const validateField = ( | ||||
|     name: string, | ||||
|     value: any, | ||||
|     rules: any, | ||||
|     option: validateOptions, | ||||
|   ): Promise<RuleError[]> => { | ||||
|     const promise = validateRules( | ||||
|       [name], | ||||
|       value, | ||||
|       rules, | ||||
|       { | ||||
|         validateMessages: defaultValidateMessages, | ||||
|         ...option, | ||||
|       }, | ||||
|       !!option.validateFirst, | ||||
|     ); | ||||
|     validateInfos[name].validateStatus = 'validating'; | ||||
|     promise | ||||
|       .catch((e: any) => e) | ||||
|       .then((results: RuleError[] = []) => { | ||||
|         if (validateInfos[name].validateStatus === 'validating') { | ||||
|           const res = results.filter(result => result && result.errors.length); | ||||
|           validateInfos[name].validateStatus = res.length ? 'error' : 'success'; | ||||
|           validateInfos[name].help = res.length ? res.map(r => r.errors) : ''; | ||||
|         } | ||||
|       }); | ||||
|     return promise; | ||||
|   }; | ||||
| 
 | ||||
|   const validate = (names?: namesType, option?: validateOptions): Promise<any> => { | ||||
|     let keys = []; | ||||
|     let strict = true; | ||||
|     if (!names) { | ||||
|       strict = false; | ||||
|       keys = rulesKeys.value; | ||||
|     } else if (Array.isArray(names)) { | ||||
|       keys = names; | ||||
|     } else { | ||||
|       keys = [names]; | ||||
|     } | ||||
|     const promises = validateFields(keys, option || {}, strict); | ||||
|     // Do not throw in console
 | ||||
|     promises.catch((e: any) => e); | ||||
|     return promises; | ||||
|   }; | ||||
| 
 | ||||
|   const clearValidate = (names?: namesType) => { | ||||
|     let keys = []; | ||||
|     if (!names) { | ||||
|       keys = rulesKeys.value; | ||||
|     } else if (Array.isArray(names)) { | ||||
|       keys = names; | ||||
|     } else { | ||||
|       keys = [names]; | ||||
|     } | ||||
|     keys.forEach(key => { | ||||
|       validateInfos[key] && | ||||
|         Object.assign(validateInfos[key], { | ||||
|           validateStatus: '', | ||||
|           help: '', | ||||
|         }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const mergeValidateInfo = (items: ValidateInfo[] | ValidateInfo) => { | ||||
|     const info = { autoLink: false } as ValidateInfo; | ||||
|     const help = []; | ||||
|     const infos = Array.isArray(items) ? items : [items]; | ||||
|     for (let i = 0; i < infos.length; i++) { | ||||
|       const arg = infos[i] as ValidateInfo; | ||||
|       if (arg?.validateStatus === 'error') { | ||||
|         info.validateStatus = 'error'; | ||||
|         arg.help && help.push(arg.help); | ||||
|       } | ||||
|       info.required = info.required || arg?.required; | ||||
|     } | ||||
|     info.help = help; | ||||
|     return info; | ||||
|   }; | ||||
|   let oldModel = initialModel; | ||||
|   const modelFn = (model: { [x: string]: any }) => { | ||||
|     const names = []; | ||||
|     rulesKeys.value.forEach(key => { | ||||
|       const prop = getPropByPath(model, key, false); | ||||
|       const oldProp = getPropByPath(oldModel, key, false); | ||||
|       if (!isEqual(prop.v, oldProp.v)) { | ||||
|         names.push(key); | ||||
|       } | ||||
|     }); | ||||
|     validate(names, { trigger: 'change' }); | ||||
|     oldModel = cloneDeep(model); | ||||
|   }; | ||||
|   const debounceOptions = options?.debounce; | ||||
|   watch( | ||||
|     modelRef, | ||||
|     debounceOptions && debounceOptions.wait | ||||
|       ? debounce(modelFn, debounceOptions.wait, omit(debounceOptions, ['wait'])) | ||||
|       : modelFn, | ||||
|     { immediate: options && !!options.immediate, deep: true }, | ||||
|   ); | ||||
| 
 | ||||
|   watch( | ||||
|     rulesRef, | ||||
|     () => { | ||||
|       if (options && options.validateOnRuleChange) { | ||||
|         validate(); | ||||
|       } | ||||
|     }, | ||||
|     { deep: true }, | ||||
|   ); | ||||
| 
 | ||||
|   return { | ||||
|     modelRef, | ||||
|     rulesRef, | ||||
|     initialModel, | ||||
|     validateInfos, | ||||
|     resetFields, | ||||
|     validate, | ||||
|     validateField, | ||||
|     mergeValidateInfo, | ||||
|     clearValidate, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export default useForm; | ||||
							
								
								
									
										2
									
								
								v2-doc
								
								
								
								
							
							
								
								
								
								
								
								
							
						
						
									
										2
									
								
								v2-doc
								
								
								
								
							|  | @ -1 +1 @@ | |||
| Subproject commit 4c298275518d5790a58d26f2ed9b83ee5ba1dba4 | ||||
| Subproject commit 7fd620c1f8c9f063af8048472d90cd7ed6c5bdd6 | ||||
		Loading…
	
		Reference in New Issue
	
	 tangjinzhou
						tangjinzhou