概述
在前端后台项目中,必然涉及到大量的表单填报,这时候必然需要表单组件,来实现快速的表单校验和填报,经过自己使用多个组件库的form组件之后,发现多数组件库的form组件库的功能都差不多,无非是各自实现方式不同而已,对于这样一个常见的组件,还是非常有必要自己实现一下的,这对于日常使用组件库的form组件有更加深刻的认识。
最终效果
组件实现原理
- 首先要明确表单组件主要功能为表单验证和表单字段输入信息的收集,第二点我们可以不用操心了,vue的v-model已经做好了,最重要的就是表单验证这一项。
- 既然要做表单验证,参考多数组件库的表单验证方案,发现基本上都是要的开源库async-validator,具体的用法可以参考文档
- 整体表单组件分为三个部分:
- form组件(主要提供给子组件表单数据和验证规则,可通过provide和inject实现)
- formItem组件(主要实现对每个表单项的表单验证)
- 自定义的表单组件(input checkbox select datePicker这里只列举了input的简单封装,其他的思路一样)
实现
在清除上面几个重要点之后,就可以具体实现了,其实也不是很复杂的
文件结构
Form.vue
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
provide() {
return {
// 将form组件注入到子组件中
formInstance: this,
};
},
props: {
// 表单提交对象
mode: {
type: Object,
default() {
return {};
},
},
// 全局设置label的宽度,如果form-item组件有定义,则优先级高于当前设置的全局的
labelWidth: {
type: Number,
},
// 整体表单规则对象
rule: {
type: Object,
default() {
return {};
},
},
},
data() {
return {};
},
methods: {
// 全部验证
async validate(callback) {
let isValidatePass = true;
for (let i = 0; i < this.$slots.default.length; i++) {
// 返回的是promise
const validateFn = this.$slots.default[i].child.validate;
// 进行表单验证
try {
await validateFn();
} catch (e) {
// 当有一项不满足的时候就通知外部
isValidatePass = false;
}
}
if (isValidatePass) {
//说明没有问题
callback(true);
} else {
callback(false);
}
},
// 清除表单验证
resetFields() {
for (let i = 0; i < this.$slots.default.length; i++) {
const resetFieldsFn = this.$slots.default[i].child.resetFields;
// 清空表单规则
resetFieldsFn()
}
},
},
};
</script>
FormItem.vue
<template>
<div :class="formItemClass">
<div
:class="formItemLabelClass"
v-if="label"
:style="computedLabeItemlStyle"
ref="formItemLabel"
>
<span>{{ label }}</span>
</div>
<div class="form-item-content" :style="formItemContent">
<slot></slot>
<div class="form-item-error-content">
<transition name="fade-content">
<div class="err-tip" v-if="errorMessage.length">
{{ errorMessage }}
</div>
</transition>
</div>
</div>
</div>
</template>
<script>
// 导入async-validator表单验证插件
import Schema from "async-validator";
export default {
// 接收form组件实例
inject: ["formInstance"],
provide() {
return {
// 将当前组件实例注入到下级子组件中,方便自定义封装的input checkbox datePicker等表单组件可以进行表单验证
formItemInstance: this,
};
},
props: {
// 表单标签
label: {
type: String,
default: "",
},
// 标签宽度
labelWidth: {
type: Number,
},
// 标签位置
labelPosition: {
type: String,
default: "",
},
// 验证表单项字段
prop: {
type: String,
default: "",
},
},
data() {
return {
// label的宽度(取值当前组件实例提供的labelWidth优先级高于form组件全局设置的labelWidth
formCotentLeft: this.labelWidth || this.formInstance.labelWidth || 0,
// 父组件传递过来的表单绑定值对象
mode: {},
// 父组件传递过来的rule规则对象
rule: {},
// 是否是必填项
required: false,
// 错误信息
errorMessage: "",
// label的高度
labelHeight: 0,
};
},
computed: {
// label样式
computedLabeItemlStyle() {
return {
width: this.labelWidth ? this.labelWidth + "px" : "",
height: this.labelHeight + "px",
};
},
// label类名
formItemLabelClass() {
return ["form-item-label", this.required ? "form-item-required" : ""];
},
// form表单项类名
formItemClass() {
return [
"form-item",
"form-item-" + (this.labelPosition ? this.labelPosition : "-default"),
];
},
//表单项内容区样式(包含表单组件和错误校验)
formItemContent() {
return {
marginLeft: (this.formCotentLeft || 100) + "px",
textAlign: this.labelPosition,
};
},
},
mounted() {
this.init();
},
methods: {
init() {
// 设置label的宽度
this.$nextTick(() => {
this.formCotentLeft = this.$refs.formItemLabel
? this.$refs.formItemLabel.scrollWidth
: 0;
this.labelHeight = this.$refs.formItemLabel.parentNode.scrollHeight;
});
//将父组件规则保留到当前组件中
this.mode = this.formInstance.mode;
this.rule = this.formInstance.rule;
// 判断当前项是否是必填项
this.checkIsRequired();
},
// 检测是否是必填项,必填项加*号
checkIsRequired() {
if (this.prop) {
let curFomItem = this.rule[this.prop] || [];
if (curFomItem) {
for (let i = 0; i < curFomItem.length; i++) {
if (curFomItem[i].required) {
this.required = true;
break;
}
}
}
}
},
// 验证表单(供form组件和自己内部调用)
async validate(type) {
return new Promise(async (resolve, reject) => {
let formItemValidate = [];
let formItemRule = this.rule[this.prop] || [];
if (type) {
formItemValidate = formItemRule.filter(
(item) => item.trigger == type
);
} else {
formItemValidate = formItemRule;
}
// 单个表单验证
let formItemValidateLen = formItemValidate.length;
if (formItemValidateLen > 0) {
// 进行表单验证
let validateCount = 0;
for (let i = 0; i < formItemValidateLen; i++) {
const validator = new Schema({
[this.prop]: formItemValidate[i],
});
try {
// 验证成功
await validator.validate({
[this.prop]: this.$parent.mode[this.prop],
});
this.errorMessage = "";
validateCount++;
} catch ({ errors, fields }) {
// 验证失败
this.errorMessage = errors[0].message;
break;
}
}
// 说明所有表单项的验证规则校验正确
if (validateCount == formItemValidateLen) {
resolve();
} else {
reject();
}
} else {
resolve();
}
});
},
// 清空表单项(只需要将错误信息清空就行)
resetFields() {
this.errorMessage = "";
},
},
};
</script>
<style lang="less">
* {
margin: 0;
padding: 0;
}
.form-item {
margin-bottom: 24px;
.form-item-label {
float: left;
padding: 0 12px;
line-height: 1;
vertical-align: middle;
position: relative;
display: flex;
align-items: center;
}
.form-item-error-content {
position: relative;
.err-tip {
position: absolute;
left: 0;
top: 0;
color: red;
}
}
}
.form-item-right {
.form-item-label {
justify-content: right;
}
}
.form-item-left {
.form-item-label {
justify-content: left;
}
}
.form-item-center {
.form-item-label {
justify-content: center;
}
}
.form-item-required:before {
content: "*";
display: inline-block;
margin-right: 4px;
line-height: 1;
font-family: SimSun;
font-size: 14px;
color: #ed4014;
}
.form-item.form-item-right {
.form-item-label {
text-align: right;
}
}
.fade-content-enter-active,
.fade-content-leave-active {
transition: opacity 0.25s;
}
.fade-content-enter,
.fade-content-leave-to {
opacity: 0;
}
</style>
index.js
import Form from "./Form.vue";
import FormItem from "./FormItem.vue";
//如果是组件库的话,也可以为每个组件加上install方法,当外部按需使用就可以使用vue.use了
export { Form, FormItem };
使用
App.vue
<template>
<div class="app">
<Form :mode="formValue" :rule="ruleCustom" :label-width="100" ref="form">
<form-item
label="姓名:"
prop="name"
:label-width="100"
label-position="right"
>
<Input type="text" v-model="formValue.name" placeholder="请输入姓名" />
</form-item>
<form-item
label="年龄:"
prop="age"
:label-width="100"
label-position="right"
>
<Input type="text" v-model="formValue.age" placeholder="请输入年龄" />
</form-item>
<form-item label="家庭住址:" :label-width="100" label-position="right">
<Input
type="text"
v-model="formValue.address"
placeholder="请输入家庭住址"
/>
</form-item>
<form-item>
<button class="btn primary" @click="handleSubmit('form')">提交</button>
<button class="btn" @click="handleCancel('form')">清除</button>
</form-item>
</Form>
</div>
</template>
<script>
import { Form, FormItem } from "./components/Form/index";
import Input from "./components/Input";
export default {
components: { Form, FormItem, Input },
data() {
return {
formValue: {
name: "小明",
age: 12,
address: "四川省成都市",
},
ruleCustom: {
name: [
{
required: true,
trigger: "blur",
validator: (rule, value, callback) => {
if (!value.length) {
callback(new Error("必填项!"));
}
if (value.length < 5) {
callback(new Error("姓名最少5个字符!"));
}
if (!/[a-zA-Z]/.test(value)) {
callback(new Error("姓名包含英文字母!"));
}
callback();
},
},
],
age: [
{
trigger: "change",
required: true,
validator: (rule, value, callback) => {
if (!value.length) {
callback(new Error("必填项!"));
}
if (value * 1 < 15) {
callback(new Error("太年轻!"));
}
if (!Number.isInteger(value * 1)) {
callback(new Error("姓名必须是数字!"));
}
if (value * 1 > 100) {
callback(new Error("年龄最多100!"));
}
callback();
},
},
],
},
};
},
methods: {
handleSubmit(name) {
this.$refs[name].validate((valid) => {
if (valid) {
this.$Message.success("成功");
} else {
this.$Message.error("失败");
}
});
},
handleCancel(name) {
this.$refs[name].resetFields();
},
},
};
</script>
<style lang="less">
input {
border: 1px solid #000;
width: 100%;
}
.btn {
display: inline-block;
margin-bottom: 0;
margin-right: 20px;
font-weight: 400;
text-align: center;
cursor: pointer;
background-image: none;
border: 1px solid transparent;
white-space: nowrap;
user-select: none;
height: 32px;
padding: 0 15px;
font-size: 14px;
border-radius: 4px;
color: #515a6e;
background-color: #fff;
border-color: #dcdee2;
}
.primary {
background-color: #008c8c;
color: #fff;
}
.app {
width: 500px;
margin: 20px auto;
}
</style>
总结
对于form组件,最重要的一点就是表单验证,因此需要熟悉async-validator的使用,其他的就是一般的逻辑代码。