ElFormItem 是 Element Plus 中的一个组件,用于在表单中显示和管理表单字段。它通常与 ElForm 组件一起使用,用于构建表单。但是,在使用 ElFormItem 时,你可能会遇到下面两个问题
问题一:错误信息中显示的prop而不是label
在使用 Element Plus 的 ElFormItem 组件时,当表单验证失败时,错误信息中显示的是 prop 属性的值,而不是 label 属性的值。这是因为 Element Plus 的 ElFormItem 组件默认情况下会将 prop 属性的值作为错误信息的一部分进行显示。但是这种显示方式可能会导致错误信息的可读性和理解性下降。
问题二:错误信息默认都是英文的
在Element Plus的中使用了async-validator进行表单验证, 而async-validator默认的错误信息都是英文的, 而不是中文的, 这就导致了错误信息的可读性和理解性下降。尤其在一些多国语言的项目中, 错误信息是需要支持多国语言的。
求人不如求己
其实在Element Plus的仓库的issues中已经有人提过这个问题, 但是一直没有解决。俗话说求人不如求己, 所以我还是自己想方法吧
目标功能
要解决上面的问题,需要实现以下功能
- 校验失败时,错误信息中显示的是 label 属性的值,而不是 prop 属性的值。
- 增加messageLabel属性,有些表单不显示label, 但是需要显示错误信息, 所以增加messageLabel属性, 用于显示错误信息。
- 支持国际化,错误信息需要支持多国语言。
上代码
// MyFormItem.ts
import { ElFormItem, formItemProps } from "element-plus";
import { defineComponent, watchEffect } from "vue";
export default defineComponent({
extends: ElFormItem,
props: {
...formItemProps,
messageLabel: {
type: String,
default: ''
}
},
setup(props, ctx) {
let exposed: any;
const render = ElFormItem.setup?.(props, {
...ctx,
expose(exp) {
exposed = exp;
ctx.expose?.(exp);
}
});
watchEffect(() => {
if (exposed.validateMessage.value && exposed.validateState.value === 'error') {
exposed.validateMessage.value = exposed.validateMessage.value.replace(props.prop, props.messageLabel || props.label);
}
});
return render;
}
})
没有办法让ElFormItem直接使用label/messageLabel占位错误信息,所以退而求其次,使用prop占位错误信息,然后在watchEffect中替换为label/messageLabel。这样最大的问题就是如果错误消息模板中出现和prop相同的字符串,可能会被错误替换。但是目前没有更好的方法。
这样在原本使用ElFormItem直接替换成MyFormItem就可以了
国际化的问题更好解决一点,async-validator支持自定义错误信息模板,这样async-validator在校验表单字段的时候就可以使用自定义的错误信息了
import Schema from 'async-validator';
Object.assign(Schema.messages, {
default: '字段 %s 验证失败',
required: '%s是必填字段',
enum: '%s 必须是 %s 中的一个',
whitespace: '%s 不能为空',
date: {
format: '%s 日期 %s 无效, 格式应为 %s',
parse: '%s 日期无法解析, %s 无效',
invalid: '%s 日期 %s 无效',
},
types: {
string: '%s 不是有效的 %s',
method: '%s 不是有效的 %s (function)',
array: '%s 不是有效的 %s',
object: '%s 不是有效的 %s',
number: '%s 不是有效的 %s',
date: '%s 不是有效的 %s',
boolean: '%s 不是有效的 %s',
integer: '%s 不是有效的 %s',
float: '%s 不是有效的 %s',
regexp: '%s 不是有效的 %s',
email: '%s 不是有效的 %s',
url: '%s 不是有效的 %s',
hex: '%s 不是有效的 %s',
},
string: {
len: '%s 长度必须为 %s 个字符',
min: '%s 长度不能少于 %s 个字符',
max: '%s长度不能超过 %s 个字符',
range: '%s 长度应在 %s 和 %s 个字符之间',
},
number: {
len: '%s 必须等于 %s',
min: '%s 不能小于 %s',
max: '%s 不能大于 %s',
range: '%s 应在 %s 和 %s 之间',
},
array: {
len: '%s 长度必须为 %s',
min: '%s 长度不能少于 %s',
max: '%s 长度不能超过 %s',
range: '%s 长度应在 %s 和 %s 之间',
},
pattern: {
mismatch: '%s 值 %s 不匹配模式 %s',
},
});
配合i18n使用就可以了,在切换语言的时候,更新这些错误信息模板就可以了
吐槽
看上面MyFromItem代码,发现的element-plus的导出中是有formItemProps的,但是ElTable, ElTableColumn组件对应的props却没有对应的导出。作为一个通过的ui组件库,每个组件导出的东西应该保持一致才好吧。
如果发现有什么问题或者有更好的解决方法,欢迎留言交流。
补充
上面的MyFormItem是通过extends来继承ElFormItem的,这种方式已经不推荐使用了。并且上面的写法在切换语言时不方便自动更新校验信息。下面提供一种新方法
<template>
<ElFormItem ref="formItem" :class="[{'hide-asterisk': hideRequiredAsterisk}]" v-bind="itemAttr">
<template v-for="(_, k) in slots" :key="k" #[k]="scope">
<slot v-bind="scope" :name="k"></slot>
</template>
<template #error="scope">
<div :class="validateClass">{{ innerFormatMessage(scope.error) }}</div>
</template>
</ElFormItem>
</template>
<script setup lang="ts">
import { ElFormItem, useNamespace, formItemProps } from 'element-plus';
import { computed, isRef, unref, useSlots, useTemplateRef, type Slots } from 'vue';
import { cut } from 'yatter';
const ns = useNamespace('form-item');
const props = defineProps({
...formItemProps,
messageLabel: {
type: String,
default: '',
},
formatMessage: {
type: Function,
default: null,
},
hideRequiredAsterisk: {
type: Boolean,
default: false,
},
});
const itemAttr = computed(() => {
const attrs: any = cut(props, Object.keys(tdformItemProps));
for (const [k, v] of Object.entries(attrs)) {
if (isRef<any>(v)) {
attrs[k] = v.value;
}
}
return attrs;
});
const formItemRef = useTemplateRef('formItem');
const slots: Slots = useSlots();
const validateClass = ns.e('error');
const propString = computed(() => {
if (!props.prop) {
return '';
}
if (Array.isArray(props.prop)) {
return props.prop.join('.');
}
return props.prop;
});
function innerFormatMessage(m: string) {
const message = m.replace(propString.value, props.messageLabel || props.label || propString.value);
return props.formatMessage?.(message) ?? message;
}
defineExpose(
new Proxy(
{},
{
get(target, key) {
return Reflect.get(formItemRef.value || {}, key);
},
has(target, key) {
return Reflect.has(formItemRef.value || {}, key);
},
},
),
);
</script>
<style lang="css" scoped>
.el-form-item.is-required.hide-asterisk.asterisk-left > :deep(.el-form-item__label)::before {
content: none;
}
.el-form-item.is-required.hide-asterisk.asterisk-right > :deep(.el-form-item__label)::after {
content: none;
}
</style>
这样使用error slot 在自定义格式化错误信息,通过async-validator自定义消息格式
required 定义成
el.errors.required\0%s
这样ElFormItem校验失败的消息将会是el.errors.required\0姓名。再通过formatMessage属性函数,解析出['el.errors.required', '姓名'] 第一参数是i18n的key值,后面是参数。也就是在formatMessage中调用i18n
MyFromItem.props.formatMessage.default = function(msg) {
// 将msg解析成参数
const args = parse(msg);
// 调用i18n
return t(...args);
}
这样使用MyFormItem替换ElFormItem就可以了