# 二.数据输入(表单)
前言 --> 表单组件特点
Form 组件分为两个部分:
- 外层的
Form
表单域组件,一组表单控件只有一个 Form - 内部包含了多个
FormItem
组件,每一个表单控件都被一个 FormItem 包裹
- 外层的
它的主要功能包括:
- 数据校验,并在对应的 FormItem 中给出校验失败的提示(
async-validator
) - 鼠标滑出下拉菜单组件,隐藏待选菜单
- 鼠标点击待选菜单中的条目,选中项文本更新,组件派发 change 事件
- 数据校验,并在对应的 FormItem 中给出校验失败的提示(
# 1.目录结构
├── form
│ ├── form-item.vue
│ ├── form.vue
│ └── index.js
2
3
4
# 2.组件封装
# 2.1 form.vue
<template>
<form class="vue-form">
<slot></slot>
</form>
</template>
<script>
export default {
name: "VueForm",
provide() {
return {
form: this,
}
},
props: {
model: {
type: Object,
},
rules: {
type: Object,
},
},
data() {
return {
fields: [],
};
},
created() {
this.$on("on-form-item-add", (field) => {
if (field) this.fields.push(field);
});
this.$on("on-form-item-remove", (filed) => {
if (field.prop) this.fields.splice(this.fields.indexOf(field), 1);
});
},
};
</script>
<style lang="scss" scoped>
.vue-form {
margin: 10px 0;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 2.2 form-item.vue
<template>
<div>
<label v-if="label">{{ label }}</label>
<div>
<slot></slot>
</div>
</div>
</template>
<script>
import Emitter from "../../emitter.js";
export default {
name: "VueFormItem",
inject: ["form"],
props: {
label: {
type: String,
default: "",
},
prop: {
type: String,
},
},
mixins: [Emitter],
mounted() {
if (this.prop) {
this.dispatch("VueForm", "on-form-item-add", this);
}
},
beforeDestory() {
this.dispatch("VueForm", "on-form-item-remove", this);
},
methods: {
setRules() {
this.$on("on-form-blur", this.onFieldBlur);
this.$on("on-form-change", this.onFieldChange);
},
mounted() {
if (this.prop) {
this.dispatch("VueForm", "on-form-item-add", this);
this.setRules();
}
},
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 3.使用案例
# 4.触发校验
// form-item.vue,部分代码省略
import AsyncValidator from "async-validator"
export default {
inject: ["form"],
props: {
prop: {
type: String,
},
},
data() {
return {
validateState: "", // 校验状态
validateMessage: "", // 校验不通过时的提示信息
}
},
computed: {
// 从 Form 的 model 中动态得到当前表单组件的数据
fieldValue() {
return this.form.model[this.prop]
},
},
methods: {
// 从 Form 的 rules 属性中,获取当前 FormItem 的校验规则
getRules() {
let formRules = this.form.rules
formRules = formRules ? formRules[this.prop] : []
return [].concat(formRules || [])
},
// 只支持 blur 和 change,所以过滤出符合要求的 rule 规则
getFilteredRule(trigger) {
const rules = this.getRules()
return rules.filter(
(rule) => !rule.trigger || rule.trigger.indexOf(trigger) !== -1
)
},
/**
* 校验数据
* @param trigger 校验类型
* @param callback 回调函数
*/
validate(trigger, callback = function () {}) {
let rules = this.getFilteredRule(trigger)
if (!rules || rules.length === 0) {
return true
}
// 设置状态为校验中
this.validateState = "validating"
// 以下为 async-validator 库的调用方法
let descriptor = {}
descriptor[this.prop] = rules
const validator = new AsyncValidator(descriptor)
let model = {}
model[this.prop] = this.fieldValue
validator.validate(model, { firstFields: true }, (errors) => {
this.validateState = !errors ? "success" : "error"
this.validateMessage = errors ? errors[0].message : ""
callback(this.validateMessage)
})
},
onFieldBlur() {
this.validate("blur")
},
onFieldChange() {
this.validate("change")
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
在 FormItem 的 validate()
方法中,最终做了两件事:
- 设置了当前的校验状态
validateState
和校验不通过提示信息validateMessage
(通过值为空); - 将 validateMessage 通过回调 callback 传递给调用者,这里的调用者是 onFieldBlur 和 onFieldChange,它们只传入了第一个参数
trigger
,callback 并未传入,因此也不会触发回调,而这个回调主要是给 Form 用的,因为 Form 中可以通过提交按钮一次性校验所有的 FormItem(后文会介绍)这里只是表单组件触发事件时,对当前 FormItem 做校验。
除了校验,还可以对当前数据进行重置。重置是指将表单组件的数据还原到最初绑定的值,而不是清空,因此需要预先缓存一份初始值。同时我们将校验信息也显示在模板中,并加一些样式。相关代码如下:
<!-- form-item.vue,部分代码省略 -->
<template>
<div>
<label v-if="label" :class="{ 'i-form-item-label-required': isRequired }">{{
label
}}</label>
<div>
<slot></slot>
<div v-if="validateState === 'error'" class="i-form-item-message">
{{ validateMessage }}
</div>
</div>
</div>
</template>
<script>
export default {
props: {
label: {
type: String,
default: "",
},
prop: {
type: String,
},
},
data() {
return {
isRequired: false, // 是否为必填
validateState: "", // 校验状态
validateMessage: "", // 校验不通过时的提示信息
}
},
mounted() {
// 如果没有传入 prop,则无需校验,也就无需缓存
if (this.prop) {
this.dispatch("VueForm", "on-form-item-add", this)
// 设置初始值,以便在重置时恢复默认值
this.initialValue = this.fieldValue
this.setRules()
}
},
methods: {
setRules() {
let rules = this.getRules()
if (rules.length) {
rules.every((rule) => {
// 如果当前校验规则中有必填项,则标记出来
this.isRequired = rule.required
})
}
this.$on("on-form-blur", this.onFieldBlur)
this.$on("on-form-change", this.onFieldChange)
},
// 从 Form 的 rules 属性中,获取当前 FormItem 的校验规则
getRules() {
let formRules = this.form.rules
formRules = formRules ? formRules[this.prop] : []
return [].concat(formRules || [])
},
// 重置数据
resetField() {
this.validateState = ""
this.validateMessage = ""
this.form.model[this.prop] = this.initialValue
},
},
}
</script>
<style>
.i-form-item-label-required:before {
content: "*";
color: red;
}
.i-form-item-message {
color: red;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
至此,FormItem 代码已经完成,不过它只具有单独校验的功能,也就是说,只能对自己的一个表单组件验证,不能对一个表单域里的所有组件一次性全部校验。而实现全部校验和全部重置的功能,要在 Form 中完成。
上文已经介绍到,在 Form
组件中,预先缓存了全部的 FormItem 实例,自然也能在 Form 中调用它们。通过点击提交按钮全部校验,或点击重置按钮全部重置数据,只需要在 Form 中,逐一调用缓存的 FormItem 实例中的 validate
或 resetField
方法。相关代码如下:
// form.vue,部分代码省略
export default {
data() {
return {
fields: [],
}
},
methods: {
// 公开方法:全部重置数据
resetFields() {
this.fields.forEach((field) => {
field.resetField()
})
},
// 公开方法:全部校验数据,支持 Promise
validate(callback) {
return new Promise((resolve) => {
let valid = true
let count = 0
this.fields.forEach((field) => {
field.validate("", (errors) => {
if (errors) {
valid = false
}
if (++count === this.fields.length) {
// 全部完成
resolve(valid)
if (typeof callback === "function") {
callback(valid)
}
}
})
})
})
},
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
虽然说 Vue.js 的 API 只来自 prop、event、slot 这三个部分,但一些场景下,需要通过 ref
来访问这个组件,调用它的一些内置方法,比如上面的 validate
和 resetFields
方法,就需要使用者来主动调用。
resetFields 很简单,就是通过循环逐一调用 FormItem 的 resetField 方法来重置数据。validate 稍显复杂,它支持两种使用方法,一种是普通的回调,比如:
<template>
<div>
<i-form ref="form"></i-form>
<button @click="handleSubmit">提交</button>
</div>
</template>
<script>
export default {
methods: {
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
window.alert("提交成功")
} else {
window.alert("表单校验失败")
}
})
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
同时也支持 Promise,例如:
handleSubmit () {
const validate = this.$refs.form.validate();
validate.then((valid) => {
if (valid) {
window.alert('提交成功');
} else {
window.alert('表单校验失败');
}
})
}
2
3
4
5
6
7
8
9
10
11
在 Form 组件定义的 Promise 中,只调用了 resolve(valid),没有调用 reject(),因此不能直接使用 .catch()
,不过聪明的你稍作修改,肯定能够支持到!
完整的用例如下:
<template>
<div>
<h3>具有数据校验功能的表单组件——Form</h3>
<i-form ref="form" :model="formValidate" :rules="ruleValidate">
<i-form-item label="用户名" prop="name">
<i-input v-model="formValidate.name"></i-input>
</i-form-item>
<i-form-item label="邮箱" prop="mail">
<i-input v-model="formValidate.mail"></i-input>
</i-form-item>
</i-form>
<button @click="handleSubmit">提交</button>
<button @click="handleReset">重置</button>
</div>
</template>
<script>
import VueForm from "../components/form/form.vue"
import VueFormItem from "../components/form/form-item.vue"
import iInput from "../components/input/input.vue"
export default {
components: { VueForm, VueFormItem, iInput },
data() {
return {
formValidate: {
name: "",
mail: "",
},
ruleValidate: {
name: [{ required: true, message: "用户名不能为空", trigger: "blur" }],
mail: [
{ required: true, message: "邮箱不能为空", trigger: "blur" },
{ type: "email", message: "邮箱格式不正确", trigger: "blur" },
],
},
}
},
methods: {
handleSubmit() {
this.$refs.form.validate((valid) => {
if (valid) {
window.alert("提交成功")
} else {
window.alert("表单校验失败")
}
})
},
handleReset() {
this.$refs.form.resetFields()
},
},
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
总结
通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响