diff --git a/components/form/Form.tsx b/components/form/Form.tsx index 4f058213fa..d8715bfd69 100755 --- a/components/form/Form.tsx +++ b/components/form/Form.tsx @@ -84,6 +84,7 @@ export const formProps = { onFieldsChange: { type: Function as PropType }, onFinish: { type: Function as PropType }, onFinishFailed: { type: Function as PropType }, + onValidate: { type: Function as PropType }, }; export type FormProps = Partial>; @@ -102,7 +103,7 @@ const Form = defineComponent({ }), Item: FormItem, useForm, - emits: ['finishFailed', 'submit', 'finish'], + emits: ['finishFailed', 'submit', 'finish', 'validate'], setup(props, { emit, slots, expose, attrs }) { const size = useInjectSize(props); const { prefixCls, direction, form: contextForm } = useConfigInject('form', props); @@ -355,6 +356,9 @@ const Form = defineComponent({ rules: computed(() => props.rules), addField, removeField, + onValidate: (name, status, errors) => { + emit('validate', name, status, errors); + }, }); watch( diff --git a/components/form/FormItem.tsx b/components/form/FormItem.tsx index a715d7e8db..efef0d9162 100644 --- a/components/form/FormItem.tsx +++ b/components/form/FormItem.tsx @@ -1,5 +1,14 @@ import type { PropType, ExtractPropTypes, ComputedRef } from 'vue'; -import { watch, defineComponent, computed, nextTick, ref, watchEffect, onBeforeUnmount } from 'vue'; +import { + watch, + defineComponent, + computed, + nextTick, + ref, + watchEffect, + onBeforeUnmount, + toRaw, +} from 'vue'; import cloneDeep from 'lodash-es/cloneDeep'; import PropTypes from '../_util/vue-types'; import Row from '../grid/Row'; @@ -213,6 +222,12 @@ export default defineComponent({ validateState.value = res.length ? 'error' : 'success'; errors.value = res.map(r => r.errors); + + formContext.onValidate( + fieldName.value, + !errors.value.length, + errors.value.length ? toRaw(errors.value[0]) : null, + ); } }); diff --git a/components/form/context.ts b/components/form/context.ts index 8031304166..35edc91fb8 100644 --- a/components/form/context.ts +++ b/components/form/context.ts @@ -19,6 +19,11 @@ export interface FormContextProps { removeField: (eventKey: string) => void; validateTrigger?: ComputedRef; rules?: ComputedRef<{ [k: string]: ValidationRule[] | ValidationRule }>; + onValidate: ( + name: string | number | string[] | number[], + status: boolean, + errors: string[] | null, + ) => void; } export const FormContextKey: InjectionKey = Symbol('formContextKey'); @@ -38,6 +43,7 @@ export const useInjectForm = () => { model: computed(() => undefined), rules: computed(() => undefined), requiredMark: computed(() => false), + onValidate: () => {}, }); }; diff --git a/components/form/demo/custom-validation.vue b/components/form/demo/custom-validation.vue index 241793a5d3..dc26c50798 100644 --- a/components/form/demo/custom-validation.vue +++ b/components/form/demo/custom-validation.vue @@ -26,6 +26,7 @@ See more advanced usage at [async-validator](https://github.com/yiminghe/async-v :rules="rules" v-bind="layout" @finish="handleFinish" + @validate="handleValidate" @finishFailed="handleFinishFailed" > @@ -112,6 +113,9 @@ export default defineComponent({ const resetForm = () => { formRef.value.resetFields(); }; + const handleValidate = (...args) => { + console.log(args); + }; return { formState, formRef, @@ -120,6 +124,7 @@ export default defineComponent({ handleFinishFailed, handleFinish, resetForm, + handleValidate, }; }, }); diff --git a/components/form/demo/useForm-basic.vue b/components/form/demo/useForm-basic.vue index 992b0b3096..80dc5c017e 100644 --- a/components/form/demo/useForm-basic.vue +++ b/components/form/demo/useForm-basic.vue @@ -72,7 +72,9 @@ export default defineComponent({ }, ], }); - const { resetFields, validate, validateInfos } = useForm(modelRef, rulesRef); + const { resetFields, validate, validateInfos } = useForm(modelRef, rulesRef, { + onValidate: (...args) => console.log(...args), + }); const onSubmit = () => { validate() .then(() => { diff --git a/components/form/index.en-US.md b/components/form/index.en-US.md index cc0244a871..1f719cdd20 100644 --- a/components/form/index.en-US.md +++ b/components/form/index.en-US.md @@ -49,6 +49,7 @@ A form consists of one or more form fields whose type includes input, textarea, | Events Name | Description | Arguments | Version | | --- | --- | --- | --- | --- | | submit | Defines a function will be called if form data validation is successful. | Function(e:Event) | | +| validate | triggers after a form item is validated | Function(name, status, errorMsgs) | | | | finish | Trigger after submitting the form and verifying data successfully | function(values) | - | 2.0.0 | | finishFailed | Trigger after submitting the form and verifying data failed | function({ values, errorFields, outOfDate }) | - | 2.0.0 | @@ -234,5 +235,10 @@ function useForm( ) => Promise; mergeValidateInfo: (items: ValidateInfo | ValidateInfo[]) => ValidateInfo; clearValidate: (names?: namesType) => void; + onValidate?: ( + name: string | number | string[] | number[], + status: boolean, + errorMsgs: string[] | null, + ) => void; }; ``` diff --git a/components/form/index.zh-CN.md b/components/form/index.zh-CN.md index 26d7ade428..439b5be562 100644 --- a/components/form/index.zh-CN.md +++ b/components/form/index.zh-CN.md @@ -50,6 +50,7 @@ cover: https://gw.alipayobjects.com/zos/alicdn/ORmcdeaoO/Form.svg | 事件名称 | 说明 | 回调参数 | 版本 | | --- | --- | --- | --- | --- | | submit | 数据验证成功后回调事件 | Function(e:Event) | | | +| validate | 任一表单项被校验后触发 | Function(name, status, errorMsgs) | | | finish | 提交表单且数据验证成功后回调事件 | function(values) | - | 2.0.0 | | finishFailed | 提交表单且数据验证失败后回调事件 | function({ values, errorFields, outOfDate }) | - | 2.0.0 | @@ -232,5 +233,10 @@ function useForm( ) => Promise; mergeValidateInfo: (items: ValidateInfo | ValidateInfo[]) => ValidateInfo; clearValidate: (names?: namesType) => void; + onValidate?: ( + name: string | number | string[] | number[], + status: boolean, + errorMsgs: string[] | null, + ) => void; }; ``` diff --git a/components/form/interface.ts b/components/form/interface.ts index 2e0380bab4..d6082fefea 100644 --- a/components/form/interface.ts +++ b/components/form/interface.ts @@ -155,6 +155,11 @@ export interface Callbacks { onFieldsChange?: (changedFields: FieldData[], allFields: FieldData[]) => void; onFinish?: (values: Values) => void; onFinishFailed?: (errorInfo: ValidateErrorEntity) => void; + onValidate?: ( + name: string | number | string[] | number[], + status: boolean, + errors: string[] | null, + ) => void; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/components/form/useForm.ts b/components/form/useForm.ts index 79f5d8e471..31c48273a4 100644 --- a/components/form/useForm.ts +++ b/components/form/useForm.ts @@ -1,5 +1,5 @@ import type { Ref } from 'vue'; -import { reactive, watch, nextTick, unref, shallowRef } from 'vue'; +import { reactive, watch, nextTick, unref, shallowRef, toRaw } from 'vue'; import cloneDeep from 'lodash-es/cloneDeep'; import intersection from 'lodash-es/intersection'; import isEqual from 'lodash-es/isEqual'; @@ -8,7 +8,7 @@ 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 { Callbacks, RuleError, ValidateMessages } from './interface'; import type { ValidateStatus } from './FormItem'; interface DebounceSettings { @@ -98,6 +98,7 @@ function useForm( deep?: boolean; validateOnRuleChange?: boolean; debounce?: DebounceSettings; + onValidate?: Callbacks['onValidate']; }, ): { modelRef: Props | Ref; @@ -252,6 +253,11 @@ function useForm( 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) : ''; + options?.onValidate?.( + name, + !res.length, + res.length ? toRaw(validateInfos[name].help[0]) : null, + ); } }); return promise;