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:
JeremyWuuuuu 2022-03-14 19:47:31 +08:00 committed by GitHub
parent 93aa1d621e
commit be0f72577b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 138 additions and 103 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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