表单校验是表单开发中最常见的一环,它可以让我们自定义校验规则,当用户填写的信息不符合校验规则时可以抛出提示,引导用户填写符合检验规则的信息。然而,如果表单校验处理地不好,如提示的方式不统一或出现了错误的提示,那么将带来糟糕的用户体验,甚至会误导用户、阻碍表单提交流程。本文将以vue2 antd框架的FormModel为例,列举几种开发中容易出现bug、影响体验的情形。
一、为什么a-form-model-item有多个子元素时,无法实时监听表单元素的blur/change事件?
开发中,有时候a-form-model-item有多个子元素,如下述代码包含了两个span和一个a-input标签:
<template>
<a-form-model ref="ruleForm" :model="form" :rules="rules">
<a-form-model-item label="提醒间隔" prop="number">
<span style="margin-right: 5px">每隔</span>
<a-input
style="display: inline-block; width: 200px; margin-right: 5px"
type="number"
v-model="form.number"
placeholder="填写数字"
></a-input>
<span>分钟提醒一次</span>
</a-form-model-item>
<a-form-model-item>
<a-button @click="onSubmit">提交</a-button>
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
data() {
return {
form: {
name: '',
},
rules: {
number: [
{required: true, message: '请填写数字', trigger: 'blur'},
],
},
};
},
methods: {
onSubmit() {
this.$refs.ruleForm.validate(valid => {
if (valid) {
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
},
},
};
</script>
上述写法会导致:当input框内容为空时,点击提交按钮后会根据rules触发校验并显示input框必填提示(这是我们期望的)。此时往input框里填写内容,期望input框失焦后提示会消失,然而并没有,提示仍然存在。我们暂且不管提示,继续点击提交按钮,发现校验通过,表单提交成功,提示消失。也就是说,当往input框里填写内容时,实际上已经满足了表单校验规则,但是提示却没有及时消失(这不是我们期望的)。 效果图如下:
当用户往我们提示需要填写的input框里填写了内容后,页面仍然提示需要填写该input框,这将会严重误导用户,让其认为自己填写错误,甚至认为页面有bug无法使用(确实有bug --)。 一开始发现这个问题,我百思不得其解,直到认真看了文档
Form.Item 会对唯一子元素进行劫持,并监听
blur
和change
事件,来达到自动校验的目的,所以请确保表单域没有其它元素包裹。如果有多个子元素,将只会监听第一个子元素的变化。
现在我们知道上面bug出现的原因了:a-form-model-item中,a-input不是第一个子元素。 根据文档,我们对上述代码中a-form-model-item部分加以修改,便可以正确地显示和隐藏提示。
<!-- ++ a-form-model-item添加ref属性 ++ -->
<a-form-model-item ref="number" label="提醒间隔" prop="number">
<span style="margin-right: 5px">每隔</span>
<!-- ++ a-input添加blur事件,调用Dom的onFieldBlur方法 ++ -->
<a-input
style="display: inline-block; width: 200px; margin-right: 5px"
type="number"
v-model="form.number"
placeholder="填写数字"
@blur="
() => {
$refs.number.onFieldBlur();
}
"
></a-input>
<span>分钟提醒一次</span>
</a-form-model-item>
效果如下:
二、综合多个表单元素的值再判断是否通过校验
为方便理解,我先举个例子。比方说有这么一个场景:有多个可多选的下拉选择器,不要求每个下拉选择器都有选中项,但要求这几个选择器中至少有一个选中项。我们可以这么写:
<template>
<a-form-model ref="ruleForm" :model="form" :rules="rules">
<a-form-model-item prop="select">
<a-form-model-item label="选择器1">
<a-select
mode="multiple"
style="width: 300px"
v-model="form.result1"
>
<a-select-option
v-for="i in 25"
:key="(i + 9).toString(36) + i"
>
{{ (i + 9).toString(36) + i }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="选择器2">
<a-select
mode="multiple"
style="width: 300px"
v-model="form.result2"
>
<a-select-option
v-for="i in 25"
:key="(i + 9).toString(36) + i"
>
{{ (i + 9).toString(36) + i }}
</a-select-option>
</a-select>
</a-form-model-item>
</a-form-model-item>
<a-form-model-item>
<a-button type="primary" @click="onSubmit">提交</a-button>
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
data() {
const checkSelect = (rule, value, callback) => {
if (this.form.result1.length + this.form.result2.length > 0) {
callback();
} else {
callback(new Error("至少选择一项"));
}
};
return {
form: {
result1: [],
result2: [],
},
rules: {
select: [
{
validator: checkSelect,
trigger: "change",
},
],
},
};
},
methods: {
onSubmit() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
return false;
}
});
},
},
};
</script>
上面代码中,一个a-form-model-item包裹了两个包含select的a-form-model-item,并加上prop属性,配置对应的rules校验。这么写也出现了第一种情况中类似的问题:只有点击按钮,校验提示才能正确的出现和消失,而触发change事件时是无法及时更新提示的。 效果如下:
我又重新翻了文档,文档中自定义校验的prop只针对单个表单元素,看来这回需要我们自己动手解决问题了。
我的思路是把这部分用prop无法解决的校验工作交给我们自己做,目标是当点击提交按钮或表单元素触发blur或change事件时,都可以正确地显示或隐藏提示信息。
首先我们仿照官方的错误提示红字写出一个组件,然后动态判断提示组件出现的时机。提示组件的作用仅仅是视觉提示而已。真正的校验拦截逻辑还是需要在提交表单事件里实现。 因为是否隐藏提示组件和提示组件所作用的表单控件是否通过校验的判断条件是一致的,所以我们可以通过提示组件显示与否,判断这部分的表单控件是否通过校验。代码和效果图如下:
<!-- 提示组件 -->
<template>
<div class="hint-container">
<slot></slot>
<transition name="drop-transform" mode="out-in">
<div v-show="show" class="text">{{ hint }}</div>
</transition>
</div>
</template>
<script>
export default {
name: "Hint",
props: {
show: {
type: Boolean,
default: false,
},
hint: {
type: String,
required: true,
},
},
};
</script>
<style scoped>
.hint-container {
position: relative;
padding-bottom: 24px;
}
.text {
position: absolute;
left: 0;
bottom: 24px;
color: #f5222f;
}
.drop-enter-active,
.drop-leave-active {
transition: opacity 0.28s;
}
.drop-enter,
.drop-leave-active {
opacity: 0;
}
/* drop-transform */
.drop-transform-leave-active,
.drop-transform-enter-active {
transition: all 0.5s;
}
.drop-transform-enter {
opacity: 0;
transform: translateY(-10px);
}
.drop-transform-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>
<!-- 表单页面 -->
<template>
<a-form-model ref="ruleForm" :model="form" :rules="rules">
<a-form-model-item label="名字" prop="name">
<a-input style="width: 300px" v-model="form.name"></a-input>
</a-form-model-item>
<hint :show="showHint" hint="至少选择一位人员">
<template #default>
<a-form-model-item label="外勤人员">
<a-select
mode="multiple"
style="width: 300px"
v-model="form.result1"
>
<a-select-option
v-for="i in 25"
:key="(i + 9).toString(36) + i"
>
{{ (i + 9).toString(36) + i }}
</a-select-option>
</a-select>
</a-form-model-item>
<a-form-model-item label="内勤人员">
<a-select
mode="multiple"
style="width: 300px"
v-model="form.result2"
>
<a-select-option
v-for="i in 25"
:key="(i + 9).toString(36) + i"
>
{{ (i + 9).toString(36) + i }}
</a-select-option>
</a-select>
</a-form-model-item>
</template>
</hint>
<a-form-model-item>
<a-button type="primary" @click="onSubmit">提交</a-button>
</a-form-model-item>
</a-form-model>
</template>
<script>
import Hint from "../components/Hint";
export default {
components: {
Hint,
},
data() {
return {
form: {
name: "",
result1: [],
result2: [],
},
rules: {
name: [
{
required: true,
trigger: "change",
message: "请填写名字",
},
],
},
};
},
computed: {
showHint() {
return this.form.result1.length + this.form.result2.length === 0;
},
},
methods: {
handleFail() {
console.log("error submit!!");
return false;
},
onSubmit() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
if (this.showHint) {
this.handleFail();
} else {
alert("submit!");
}
} else {
this.handleFail();
}
});
},
},
};
</script>
从效果图可以看出,hint组件实现了正确显示校验信息的目标,并且ui效果和官方基本一致。然而它或许有一个瑕疵:一开始展示表单的时候,提示信息就已经出现了,这点和官方的提示信息出现逻辑是不一样的。有两个点值得思考:
1.复杂表单场景下如何更好地让用户知晓表单的填写规则。 基于此考虑,上述例子中,一开始就展示提示信息似乎是不错的选择,而每个选择器的label都打上星号反而不妥。
2.是否有更好的表单展示方式。 上述例子中,我们用多个可多选的选择器来构造表单,最终期望用户任意地最少选择一个值。但其实我们可以用可多选的树来代替,如下图:
那么,用树来代替多个选择器就是更好的吗?答案是不一定。有时候,单个选择器有成百上千条数据源,把几个选择器的海量数据源都揉进树里显然是行不通的,因此选择哪种方案还得具体情况具体分析。
三、隐藏的元素仍然触发校验
有时候,我们会根据表单元素x的值,来动态显示隐藏表单元素y,当y显示时,我们希望对它添加校验,当y隐藏时,则不需要校验。本着用v-show减少初始渲染开销的考虑,写出如下代码:
<template>
<a-form-model ref="ruleForm" :model="form" :rules="rules">
<a-form-model-item label="是否需要提醒">
<a-radio-group v-model="form.needReminder">
<a-radio value="1">是</a-radio>
<a-radio value="0">否</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item
v-show="form.needReminder === '1'"
label="提醒方式"
prop="type"
>
<a-radio-group v-model="form.type">
<a-radio value="1">方式1</a-radio>
<a-radio value="2">方式2</a-radio>
</a-radio-group>
</a-form-model-item>
<a-form-model-item>
<a-button type="primary" @click="onSubmit">提交</a-button>
</a-form-model-item>
</a-form-model>
</template>
<script>
export default {
data() {
return {
form: {
needReminder: "0",
type: "",
},
rules: {
type: [
{
required: true,
message: "请选择提醒方式",
trigger: "change",
},
],
},
};
},
methods: {
onSubmit() {
this.$refs.ruleForm.validate((valid) => {
if (valid) {
alert("submit!");
} else {
console.log("error submit!!");
return false;
}
});
},
},
};
</script>
上述代码存在的问题是,当我们选择了不需要提醒的时候,隐藏的选择提醒方式的单选框仍然触发了校验,于是报错、无法提交。效果如下:
这个问题的解决方法很简单,只需要把v-show换成v-if就好了。但是如此一来,如果用v-if频繁地动态显示和隐藏,就会多次重新渲染dom。好在这种情况不会太经常出现。
总结
以上介绍了几种表单校验情景,标题说是踩坑,其实坑不在ui框架,而是我们需要用心对待,否则会自己挖坑。有次测试人员测试我开发的表单,说流程跑不下去了,总是报错,无法提交。我一楞,自己明明调试过的。其实,测试人员说的流程跑不下去的表单是可以通过校验的,但是出现了上述例子中提示信息没有及时更新的问题。归根结底,是自己对一些错误的表单提示视而不见,想当然地以为用户应该会漠视错误提示,先去点提交吧?自那以后,我才深刻体会到表单校验的规范有多重要,毕竟,我们开发的目的在于给用户提供更好的体验,这也是写下这篇文章的初衷。