前言:通⽤表单组件(form表单)是各大项目中几乎是非常常用的了,那么我们怎么不依赖ui框架自己去实现一个呢?实现收集数据、校验数据并提交。本文详细记录了实现的每一个步骤及优化。
需要的技术支持
- dispatch(componentName)
- provide/inject
- async-validator
- $attrs
- mixins
用法及分析
我们自定义用法示例:
<SwForm :model='userInfo' :rules="rules" ref='swform'>
<SwFormItem label="用户名:" prop='acc'>
<SwFormInput
v-model="userInfo.acc"
placeholder="请输入用户名"
></SwFormInput>
</SwFormItem>
<SwFormItem label="密码:" prop='pwd'>
<SwFormInput
type="password"
v-model="userInfo.pwd"
placeholder="请输入用户名"
></SwFormInput>
</SwFormItem>
<SwFormItem>
<button @click="login">登录</button>
</SwFormItem>
</SwForm>
复制代码
data() {
return {
userInfo: {
acc: "",
pwd: "",
},
rules: {
acc: [{ required: true, message: "用户名不得低于5位", min: 5 }],
pwd: [
{
required: true,
message: "密码不低于5位不超过10位",
min: 5,
max: 10,
},
],
},
};
},
复制代码
需求分析
- 首先有输入框input,只是用来输入信息的,有type、placeholder等属性,在使用的地方v-model双向绑定。
- 然后有formitem,有label、prop属性,还应该有校验、错误提示信息
- 然后还有form,用来做全局校验
SwFormInput
新建components/form
文件夹,再新建SwFormInput.vue
、index.vue
文件。
index.vue文件:
<template>
<div>
<SwFormInput
v-model="userInfo.acc"
placeholder="请输入用户名"
></SwFormInput>
<!-- 测试双向绑定 -->
<p>{{ userInfo.acc }}</p>
</div>
</template>
<script>
import SwFormInput from "./SwFormInput";
export default {
components: { SwFormInput },
data() {
return {
userInfo: {
acc: "",
},
rules: {
acc: [
{
required: true,
message: "用户名不低于5位不超过10位",
min: 5,
max: 10,
},
],
},
};
},
};
</script>
复制代码
SwFormInput.vue文件:
<template>
<div>
<template>
<!-- 因为可能不同的input传递的属性是不一样的,所以使用$attrs来展开获得 -->
<input type="text" @input="input" v-bind="$attrs" />
</template>
</div>
</template>
<script>
export default {
methods: {
input() {
// 实现双向绑定需要实现: 1.:value 2. 监听input事件
this.$emit("input", event.target.value);
},
},
};
</script>
复制代码
这个时候我们发现,SwFormInput文件中div也加上了placeholder属性,这个叫属性的继承。在script中加上一个属性:inheritAttrs设置为false,关闭属性继承,避免设置到根元素上。
SwFormItem
新建SwFormItem.vue文件,SwFormItem文件中接收有label、prop属性,还应该有校验、错误提示信息。
<template>
<div>
<!-- label标签 -->
<label v-if="label">{{ label }}</label>
<!-- 插槽:SwFormInput的位置 -->
<slot></slot>
<!-- 验证不通过时候的信息 -->
<p v-if="error">{{ error }}</p>
</div>
</template>
<script>
export default {
data() {
return {
error: "", // 验证失败的提示信息,应该是这个组件自己的状态,所以就让组件自己来维护
};
},
props: {
label: {
type: String,
default: "",
},
prop: {
type: String,
default: "",
},
},
};
</script>
复制代码
在index.vue中引入SwFormItem并使用它。
<SwFormItem label='用户名' prop='acc'>
<SwFormInput
v-model="userInfo.acc"
placeholder="请输入用户名"
></SwFormInput>
</SwFormItem>
import SwFormItem from "./SwFormItem";
components: { SwFormInput, SwFormItem },
复制代码
这个时候,我们能去做验证吗?思考一下,输入框的值,和验证规则,现在在SwFormItem这个组件中能拿取到吗?貌似好像不行是吧。有没有想着,那简单,在index.vue中,把值和规则全传递过来不就完了么,但是第一我们书写的语法就不一样了,第二是每个SwFormItem中都拿取全部的值和全部的规则吗?顺再思考一下,我们传递了prop属性,用来干嘛呢?带着这些疑惑,我们再来做SwForm。
SwForm
新建SwForm.vue文件,SwForm中接收model、rules属性。
<template>
<div>
<!-- 插槽: 显示标签内所有的内容 -->
<slot></slot>
</div>
</template>
<script>
export default {
props: {
model: {
// 使用SwForm时候传递的所有数据
type: Object,
require: true,
},
rules: {
// 使用SwForm时候传递的所有验证规则
type: Object,
},
},
};
</script>
复制代码
同理在index.vue中引入并使用。
<SwForm :model="userInfo" :rules="rules">
<SwFormItem label="用户名" prop="acc">
<SwFormInput
v-model="userInfo.acc"
placeholder="请输入用户名"
></SwFormInput>
</SwFormItem>
</SwForm>
import SwForm from "./SwForm";
components: { SwFormInput, SwFormItem, SwForm },
复制代码
这个时候,我们还做什么操作呢?我们将组件实例作为提供者,⼦代组件可⽅便获取数据和校验规则。provide/inject能够实现祖先和后代之间传值,当我们不使用vuex时,vue提供给了我们这种原生接口的方式来实现隔代传值。
// script中添加provide
provide() { // 提供的意思
// 隔代传参,用法类似于data
return {
form: this, // 把组件实例本身提供给子孙组件
};
},
复制代码
SwForm做为提供者,把自身提供给了子孙组件,我们就在SwFormItem中来注入获取它。
SwFormItem.vue中新增:
inject: ['form'], // 注入,注入需要的属性
复制代码
验证
SwFormInput中,监听change事件
<!-- 每次值更改了并且失去焦点就通知验证 那么如何去通知呢? 使用$parent会导致耦合度很高,适应性不强-->
<input type="text" @input="input" v-bind="$attrs" @change="change" />
复制代码
src下新建mixins混入文件,新增emitter.js,把上面的官方代码复制进去。
SwFormInput中:
<script>
import emitter from "@/mixins/emitter.js";
export default {
mixins: [emitter],
inheritAttrs: false,
methods: {
input() {
// 实现双向绑定需要实现: 1.:value 2. 监听input事件
this.$emit("input", event.target.value);
},
change() {
// this.dispatch: 参数1: 组件的componentName 参数2: 触发的方法
// 在需要触发的组件中 写上componentName,注意不是name -> 在SwFormItem组件中写上componentName:'SwFormItem',
this.dispatch('SwFormItem', 'validate');
}
},
};
</script>
复制代码
SwFormItem的script中添加:
mounted() {
this.$on("validate", () => {
this.validate();
});
},
methods: {
validate() {
console.log("validate");
},
},
复制代码
测试是否成功。没问题之后,我们开始验证。
在validate方法中,我们需要做验证,第一:怎么拿取值和规则;第二:有了规则和值怎么验证。
先来看第一步,怎么拿取值和对应的规则。我们通过provide/inject传递并接收了form实例,在这儿就可以使用拿取值了。
validate() {
const rule = this.form.rules[this.prop];
const value = this.form.model[this.prop];
},
复制代码
拿取到值和规则之后,我们就可以进行下一步:验证了。
使用插件async-validator
。
安装:
npm i async-validator -D
复制代码
使用:
import Schema from "async-validator";
复制代码
mounted() {
// 监听验证的通知进行验证
this.$on("validate", () => {
this.validate().catch(() => {
// 为了防止promise错误不被捕获而报错
console.log();
});
});
},
methods: {
validate() {
// 获取规则并执⾏校验
// 拿取值
const value = this.form.model[this.prop];
// 拿取校验规则
const rule = this.form.rules[this.prop];
// 创建校验器
// Schema的参数:key是校验字段,value是校验规则
const validator = new Schema({ [this.prop]: rule });
// 执行校验, 返回Promise,没有触发catch就说明验证通过
return new Promise((resolve, reject) => {
// validate参数: key是校验字段, value是校验值
validator.validate({ [this.prop]: value }, (err) => {
if (err) {
// 如果有错误,校验失败
this.error = err[0].message;
reject(err[0].message); // 抛出错误
} else {
this.error = "";
resolve();
}
});
});
},
},
复制代码
全局验证
index.vue中添加一个button:
<SwForm :model="userInfo" :rules="rules" ref="swform">
<SwFormItem label="用户名" prop="acc">
<SwFormInput
v-model="userInfo.acc"
placeholder="请输入用户名"
></SwFormInput>
</SwFormItem>
<SwFormItem>
<button @click="login">登录</button>
</SwFormItem>
</SwForm>
复制代码
methods: {
login() {
// 找到SwForm实例,调用它的validate方法,会得到结果 -> 成功或者失败
this.$refs.swform.validate((result) => {
if (result) {
alert("suc");
} else {
alert("err");
}
});
},
},
复制代码
SwForm.vue中:
methods: {
validate(callback) {
// 遍历所有儿子中含有prop属性的,然后执行他们的validate方法 -> 方法会返回promise -> 放到一个数组validates中
const validates = this.$children
.filter((item) => item.prop)
.map((item) => item.validate());
Promise.all(validates) // 使用all方法全部执行,全部成功才算校验通过,有一个失败就失败
.then(() => callback(true))
.catch(() => callback(false));
},
},
复制代码
到此,我们就完成了element的form表单的自定义实现。
优化
在上一步SwForm.vue中:validate方法里面通过this.$children获取并遍历含有prop属性的,然后执行他们的validate方法,要递归获取所有的子组件,这个是会影响性能的。官方的做法是: 每个需要校验的SwFormItem实例,向SwForm传递自身,然后在SwForm中就可以拿取到所有的需要校验的SwFormItem组件实例了。
做法:SwFormItem.vue中:
import emitter from '@/mixins/emitter';
mixins: [emitter],
mounted() {
// 监听验证的通知进行验证
this.$on("validate", () => {
this.validate().catch(() => {
// 为了防止promise错误不被捕获而报错
console.log();
});
});
if(this.prop) { // 如果有prop属性 那么就代表需要进行验证
// 派发事件通知SwForm,在SwForm中新增自己 -> 把自己传递过去
this.dispatch('SwForm', 'formItenField', [this])
}
},
复制代码
SwForm中:
componentName: 'SwForm',
data() {
return {
field: [], // 用来存放所有需要校验的SwFormItem组件
};
},
created() {
this.$on("formItenField", (item) => {
this.field.push(item);
});
},
validate(callback) {
const validates = this.field.map((item) => item.validate());
Promise.all(validates)
.then(() => callback(true))
.catch(() => callback(false));
},
复制代码
样式
样式就自己微调了