mirror of
https://github.com/element-plus/element-plus.git
synced 2025-01-30 11:16:12 +08:00
fix(components): [el-form] Form not emitting validation result (#6610)
* fix(components): [el-form] Form not emitting validation result - Refactor validate code to allow it to yield promise value - Update tests accordingly * fix: refactor Co-authored-by: 三咲智子 <sxzz@sxzz.moe>
This commit is contained in:
parent
93aa1d621e
commit
be0f72577b
@ -259,12 +259,16 @@ describe('Form', () => {
|
||||
},
|
||||
})
|
||||
const form = wrapper.findComponent(Form).vm as FormInstance
|
||||
form.validate(async (valid) => {
|
||||
expect(valid).toBe(false)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.el-form-item__error').exists()).toBe(false)
|
||||
done()
|
||||
})
|
||||
form
|
||||
.validate(async (valid) => {
|
||||
expect(valid).toBe(false)
|
||||
await nextTick()
|
||||
expect(wrapper.find('.el-form-item__error').exists()).toBe(false)
|
||||
done()
|
||||
})
|
||||
.catch((e) => {
|
||||
expect(e).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
test('reset field', async () => {
|
||||
@ -504,7 +508,7 @@ describe('Form', () => {
|
||||
],
|
||||
})
|
||||
return () => (
|
||||
<Form ref="formRef" model={form} rules={rules}>
|
||||
<Form ref="formRef" model={form} rules={rules.value}>
|
||||
<FormItem ref="age" prop="age" label="age">
|
||||
<Input v-model={form.age} />
|
||||
</FormItem>
|
||||
@ -517,7 +521,7 @@ describe('Form', () => {
|
||||
.validate()
|
||||
.catch(() => undefined)
|
||||
const ageField = wrapper.findComponent({ ref: 'age' })
|
||||
expect((ageField.vm as FormItemInstance).validateState).toBe('success')
|
||||
expect(ageField.classes('is-success')).toBe(true)
|
||||
expect(ageField.classes()).toContain('is-success')
|
||||
})
|
||||
})
|
||||
|
@ -59,15 +59,11 @@ import { formItemProps } from './form-item'
|
||||
import FormLabelWrap from './form-label-wrap'
|
||||
|
||||
import type { CSSProperties } from 'vue'
|
||||
import type {
|
||||
RuleItem,
|
||||
ValidateError,
|
||||
ValidateFieldsError,
|
||||
} from 'async-validator'
|
||||
import type { RuleItem } from 'async-validator'
|
||||
import type { FormItemContext } from '@element-plus/tokens'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
import type { FormItemValidateState } from './form-item'
|
||||
import type { FormItemRule } from './types'
|
||||
import type { FormItemRule, FormValidateFailure } from './types'
|
||||
|
||||
const COMPONENT_NAME = 'ElFormItem'
|
||||
defineOptions({
|
||||
@ -208,64 +204,63 @@ const currentLabel = computed(
|
||||
() => `${props.label || ''}${formContext.labelSuffix || ''}`
|
||||
)
|
||||
|
||||
const validate: FormItemContext['validate'] = async (trigger, callback) => {
|
||||
if (callback) {
|
||||
try {
|
||||
validate(trigger)
|
||||
callback(true)
|
||||
} catch (err) {
|
||||
callback(false, err as ValidateFieldsError)
|
||||
}
|
||||
const setValidationState = (state: FormItemValidateState) => {
|
||||
validateState.value = state
|
||||
}
|
||||
|
||||
validate(trigger)
|
||||
.then(() => callback(true))
|
||||
.catch((fields: ValidateFieldsError) => callback(false, fields))
|
||||
return
|
||||
const onValidationFailed = (error: FormValidateFailure) => {
|
||||
const { errors, fields } = error
|
||||
if (!errors || !fields) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
if (!validateEnabled.value) {
|
||||
return
|
||||
}
|
||||
const rules = getFilteredRule(trigger)
|
||||
if (rules.length === 0) {
|
||||
return
|
||||
}
|
||||
setValidationState('error')
|
||||
validateMessage.value = errors
|
||||
? errors?.[0]?.message ?? `${props.prop} is required`
|
||||
: ''
|
||||
|
||||
validateState.value = 'validating'
|
||||
|
||||
const descriptor = {
|
||||
[propString.value]: rules,
|
||||
}
|
||||
const validator = new AsyncValidator(descriptor)
|
||||
const model = {
|
||||
[propString.value]: fieldValue.value,
|
||||
}
|
||||
|
||||
interface ValidateFailure {
|
||||
errors: ValidateError[] | null
|
||||
fields: ValidateFieldsError
|
||||
}
|
||||
formContext.emit('validate', props.prop!, !errors, validateMessage.value)
|
||||
}
|
||||
|
||||
const doValidate = async (rules: RuleItem[]): Promise<true> => {
|
||||
const modelName = propString.value
|
||||
const validator = new AsyncValidator({
|
||||
[modelName]: rules,
|
||||
})
|
||||
return validator
|
||||
.validate(model, { firstFields: true })
|
||||
.validate({ [modelName]: fieldValue.value }, { firstFields: true })
|
||||
.then(() => {
|
||||
validateState.value = 'success'
|
||||
setValidationState('success')
|
||||
return true as const
|
||||
})
|
||||
.catch((err: ValidateFailure) => {
|
||||
const { errors, fields } = err
|
||||
if (!errors || !fields) console.error(err)
|
||||
.catch((err: FormValidateFailure) => {
|
||||
onValidationFailed(err as FormValidateFailure)
|
||||
return Promise.reject(err)
|
||||
})
|
||||
}
|
||||
|
||||
validateState.value = 'error'
|
||||
validateMessage.value = errors
|
||||
? errors[0].message || `${props.prop} is required`
|
||||
: ''
|
||||
formContext.emit('validate', props.prop!, !errors, validateMessage.value)
|
||||
const validate: FormItemContext['validate'] = async (trigger, callback) => {
|
||||
if (!validateEnabled.value) return false
|
||||
|
||||
const rules = getFilteredRule(trigger)
|
||||
if (rules.length === 0) return true
|
||||
|
||||
setValidationState('validating')
|
||||
|
||||
return doValidate(rules)
|
||||
.then(() => {
|
||||
callback?.(true)
|
||||
return true as const
|
||||
})
|
||||
.catch((err: FormValidateFailure) => {
|
||||
const { fields } = err
|
||||
callback?.(false, fields)
|
||||
return Promise.reject(fields)
|
||||
})
|
||||
}
|
||||
|
||||
const clearValidate: FormItemContext['clearValidate'] = () => {
|
||||
validateState.value = ''
|
||||
setValidationState('')
|
||||
validateMessage.value = ''
|
||||
}
|
||||
|
||||
@ -281,13 +276,14 @@ watch(
|
||||
() => props.error,
|
||||
(val) => {
|
||||
validateMessage.value = val || ''
|
||||
validateState.value = val ? 'error' : ''
|
||||
setValidationState(val ? 'error' : '')
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.validateStatus,
|
||||
(val) => (validateState.value = val || '')
|
||||
(val) => setValidationState(val || '')
|
||||
)
|
||||
|
||||
const context: FormItemContext = reactive({
|
||||
@ -299,6 +295,7 @@ const context: FormItemContext = reactive({
|
||||
clearValidate,
|
||||
validate,
|
||||
})
|
||||
|
||||
provide(formItemContextKey, context)
|
||||
|
||||
onMounted(() => {
|
||||
@ -307,6 +304,7 @@ onMounted(() => {
|
||||
initialValue = clone(fieldValue.value)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
formContext.removeField(context)
|
||||
})
|
||||
|
@ -6,14 +6,14 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, provide, reactive, toRefs, watch } from 'vue'
|
||||
import { debugWarn } from '@element-plus/utils'
|
||||
import { debugWarn, type Arrayable } from '@element-plus/utils'
|
||||
import { formContextKey } from '@element-plus/tokens'
|
||||
import { useNamespace, useSize } from '@element-plus/hooks'
|
||||
import { formProps, formEmits } from './form'
|
||||
import { useFormLabelWidth, filterFields } from './utils'
|
||||
import type { ValidateFieldsError } from 'async-validator'
|
||||
import type { FormItemContext, FormContext } from '@element-plus/tokens'
|
||||
import type { FormValidateCallback } from './types'
|
||||
import type { FormValidateCallback, FormValidationResult } from './types'
|
||||
import type { FormItemProp } from './form-item'
|
||||
|
||||
const COMPONENT_NAME = 'ElForm'
|
||||
@ -61,55 +61,71 @@ const clearValidate: FormContext['clearValidate'] = (props = []) => {
|
||||
filterFields(fields, props).forEach((field) => field.clearValidate())
|
||||
}
|
||||
|
||||
const validate = async (callback?: FormValidateCallback): Promise<void> =>
|
||||
validateField(undefined, callback)
|
||||
|
||||
const validateField: FormContext['validateField'] = async (
|
||||
properties = [],
|
||||
callback
|
||||
) => {
|
||||
if (callback) {
|
||||
validate()
|
||||
.then(() => callback(true))
|
||||
.catch((fields: ValidateFieldsError) => callback(false, fields))
|
||||
return
|
||||
const isValidatable = computed(() => {
|
||||
const hasModel = !!props.model
|
||||
if (!hasModel) {
|
||||
debugWarn(COMPONENT_NAME, 'model is required for validate to work.')
|
||||
}
|
||||
return hasModel
|
||||
})
|
||||
|
||||
const { model, scrollToError } = props
|
||||
const obtainValidateFields = (props: Arrayable<FormItemProp>) => {
|
||||
if (fields.length === 0) return []
|
||||
|
||||
if (!model) {
|
||||
debugWarn(COMPONENT_NAME, 'model is required for form validation!')
|
||||
return
|
||||
}
|
||||
if (fields.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const filteredFields = filterFields(fields, properties)
|
||||
const filteredFields = filterFields(fields, props)
|
||||
if (!filteredFields.length) {
|
||||
debugWarn(COMPONENT_NAME, 'please pass correct props!')
|
||||
return
|
||||
return []
|
||||
}
|
||||
return filteredFields
|
||||
}
|
||||
|
||||
let valid = true
|
||||
let invalidFields: ValidateFieldsError = {}
|
||||
let firstInvalidFields: ValidateFieldsError | undefined
|
||||
const validate = async (
|
||||
callback?: FormValidateCallback
|
||||
): FormValidationResult => validateField(undefined, callback)
|
||||
|
||||
for (const field of filteredFields) {
|
||||
const fieldsError = await field
|
||||
.validate('')
|
||||
.catch((fields: ValidateFieldsError) => fields)
|
||||
const doValidateField = async (
|
||||
props: Arrayable<FormItemProp> = []
|
||||
): Promise<boolean> => {
|
||||
if (!isValidatable.value) return false
|
||||
|
||||
if (fieldsError) {
|
||||
valid = false
|
||||
if (!firstInvalidFields) firstInvalidFields = fieldsError
|
||||
const fields = obtainValidateFields(props)
|
||||
if (fields.length === 0) return true
|
||||
|
||||
let validationErrors: ValidateFieldsError = {}
|
||||
for (const field of fields) {
|
||||
try {
|
||||
await field.validate('')
|
||||
} catch (fields) {
|
||||
validationErrors = {
|
||||
...validationErrors,
|
||||
...(fields as ValidateFieldsError),
|
||||
}
|
||||
}
|
||||
|
||||
invalidFields = { ...invalidFields, ...fieldsError }
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
if (scrollToError) scrollToField(Object.keys(firstInvalidFields!)[0])
|
||||
if (Object.keys(validationErrors).length === 0) return true
|
||||
return Promise.reject(validationErrors)
|
||||
}
|
||||
|
||||
const validateField: FormContext['validateField'] = async (
|
||||
modelProps = [],
|
||||
callback
|
||||
) => {
|
||||
try {
|
||||
const result = await doValidateField(modelProps)
|
||||
// When result is false meaning that the fields are not validatable
|
||||
if (result === true) {
|
||||
callback?.(result)
|
||||
}
|
||||
return result
|
||||
} catch (e) {
|
||||
const invalidFields = e as ValidateFieldsError
|
||||
|
||||
if (props.scrollToError) {
|
||||
scrollToField(Object.keys(invalidFields)[0])
|
||||
}
|
||||
callback?.(false, invalidFields)
|
||||
return Promise.reject(invalidFields)
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,8 @@
|
||||
import type { RuleItem, ValidateFieldsError } from 'async-validator'
|
||||
import type {
|
||||
RuleItem,
|
||||
ValidateError,
|
||||
ValidateFieldsError,
|
||||
} from 'async-validator'
|
||||
import type { Arrayable } from '@element-plus/utils'
|
||||
import type { useFormLabelWidth } from './utils'
|
||||
|
||||
@ -7,9 +11,14 @@ export interface FormItemRule extends RuleItem {
|
||||
}
|
||||
export type FormRules = Partial<Record<string, Arrayable<FormItemRule>>>
|
||||
|
||||
export type FormValidationResult = Promise<boolean>
|
||||
export type FormValidateCallback = (
|
||||
isValid: boolean,
|
||||
invalidFields?: ValidateFieldsError
|
||||
) => void
|
||||
export interface FormValidateFailure {
|
||||
errors: ValidateError[] | null
|
||||
fields: ValidateFieldsError
|
||||
}
|
||||
|
||||
export type FormLabelWidthContext = ReturnType<typeof useFormLabelWidth>
|
||||
|
@ -92,7 +92,11 @@
|
||||
</span>
|
||||
<el-icon
|
||||
v-if="validateState && validateIcon && needStatusIcon"
|
||||
:class="[nsInput.e('icon'), nsInput.e('validateIcon')]"
|
||||
:class="[
|
||||
nsInput.e('icon'),
|
||||
nsInput.e('validateIcon'),
|
||||
nsInput.is('loading', validateState === 'validating'),
|
||||
]"
|
||||
>
|
||||
<component :is="validateIcon" />
|
||||
</el-icon>
|
||||
|
@ -5,6 +5,7 @@ import type {
|
||||
FormEmits,
|
||||
FormItemProp,
|
||||
FormItemProps,
|
||||
FormValidationResult,
|
||||
FormValidateCallback,
|
||||
FormLabelWidthContext,
|
||||
} from '@element-plus/components/form'
|
||||
@ -22,14 +23,17 @@ export type FormContext = FormProps &
|
||||
validateField: (
|
||||
props?: Arrayable<FormItemProp>,
|
||||
callback?: FormValidateCallback
|
||||
) => Promise<void>
|
||||
) => FormValidationResult
|
||||
}
|
||||
|
||||
export interface FormItemContext extends FormItemProps {
|
||||
$el: HTMLDivElement | undefined
|
||||
size: ComponentSize
|
||||
validateState: string
|
||||
validate: (trigger: string, callback?: FormValidateCallback) => Promise<void>
|
||||
validate: (
|
||||
trigger: string,
|
||||
callback?: FormValidateCallback
|
||||
) => FormValidationResult
|
||||
resetField(): void
|
||||
clearValidate(): void
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user