本文从零开始实现一个自定义的vue2.x表单组件my-form ,组件使用体验类似element-ui
实现过程涉及到的知识点
- 自定义事件
- 事件的广播与派发
- v-mode语法糖原理
- $attrs,参考https://cn.vuejs.org/v2/api/#vm-attrs
- provide/inject传递数据,参考https://cn.vuejs.org/v2/api/#provide-inject
需求拆解
- 实现组件
my-form ,处理表单整体校验、表单data维护,表单rules校验规则维护 - 实现组件
my-form-item ,处理单个表单项组件的校验,显示表单label, 校验错误信息 - 实现组件
my-input 用于测试表单组件
my-form框架
新建my-form.vue,实现拆解需求提供的功能
- 接受model,保存表单数据
- 接受校验规则
- 提供表单整体校验方法validate,调用子组件
my-form-item 的校验方法
先上一段伪代码,展示组件基本结构
<template>
<div>
<slot></slot>
</div>
</template>
<script>
export default {
props: {
model: {
type: Object,
required: true,
},
rules: Object,
},
methods: {
validate () {},
},
};
</script>
my-form-item框架
async-validator三方库实现校验,antd和ElementUi也是使用的这个库,用法参考https://www.npmjs.com/package/async-validator
- 接受
label ,用于显示表单项文本 - 接受
prop ,当前表单项的key,用于获取校验规则、表单项的值。 - 提供
validate 方式,校验当前表单项 - 注册自定事件validate,表单项的具体控件如
my-input 在blur 或者change 时调用该方法进行校验
<template>
<div>
div
<label v-if="label">{{ label }}</label>
<!-- 显示表单元素 -->
<slot></slot>
<!-- 显示错误信息 -->
<p v-if="error"
class="error">{{ error }}</p>
</div>
</template>
<script>
import Schema from "async-validator";
export default {
props: {
label: {
type: String,
default: "",
},
prop: {
type: String,
default: "",
},
},
data () {
return {
error: "",
};
},
mounted () {
this.$on("validate", () => {
this.validate();
});
},
methods: {
validate () {},
},
};
</script>
<style lang="less">
.error {
color: red;
}
</style>
my-form-item组件校验
校验疑问:校验的过程其实就是规则和表单项的值进行匹配,但是my-form-item 组件又没有保存表单项的值,该怎么办呢?回想下在使用ElementUI的时候,我们并没有显示传递表单项的值,她是怎样做到呢,其实是通过provide/inject实现的。
在my-form 中将实例provide 给子孙后代
provide () {
return {
form: this,
};
},
在子孙后代组件my-form-item 中通过inject 接受
import Schema from "async-validator";
export default {
inject: ["form"],
methods: {
validate () {
if(!this.prop) return
const rules = this.form.rules[this.prop];
rules.forEach(item=>Reflect.deleteProperty(item, 'trigger'))
const value = this.form.model[this.prop];
const validator = new Schema({ [this.prop]: rules });
return validator.validate({ [this.prop]: value }, (errors) => {
if (errors) {
this.error = errors[0].message;
} else {
this.error = "";
}
});
},
},
};
my-form组件校验
my-form-item 组件已基本实现校验,继续把目光放到my-form组件,它的校验思路是:
methods: {
validate (cb) {
const tasks = this.$children
.filter((item) => item.prop)
.map((item) => item.validate());
Promise.all(tasks)
.then(() => cb(true))
.catch(() => {
console.log("catch-false");
cb(false);
});
},
},
在收集my-form-item 的validate 方法时,我们使用this.$children 获取子组件,这里会有一个很大的问题,子组件my-form-item 和my-form 有可能不是直接父子关系,他们之间可能有其他组件或元素,所以我们需要一个方法去递归遍历my-form 的所有子元素,找出所有的my-form-item 触发validate。其代码实现过程拆分成如下几个部分:
-
定义组件标识,要找出item组件,首先我们要给所有item组件加一个标识(组件名称),此处继续参考elementUI,每个组件都有一个componentName属性
componentName: "my-form-item",
-
定义广播方法broadcast ,用于递归遍历子元素,找出目标组件,触发目标事件
broadcast事件广播
定义broadcast方法递归遍历子元素,找出目标组件,触发目标事件,然后将其写入一个mixins里,方便每一个组件使用,
新建一个emitter.js 文件:
function broadcast(componentName, eventName, params) {
this.$children.forEach((child) => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
methods: {
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
},
},
};
用broadcast 替换my-form.vue 中this.$children 的写法,解决父子组件耦合关系
mixins:[emitter],
methods: {
validate (cb) {
const tasks = this.broadcast('my-form-item','validate','')
Promise.all(tasks)
.then(() => cb(true))
.catch(() => {
console.log("catch-false");
cb(false);
});
},
},
my-input组件
input组件功能较为简单,主要是两个功能点
- 实现v-model
- blur和input事件触发校验
- $attrs普通属性的传递
<template>
<div>
<input :type="type"
:value="value"
@input="onInput"
@blur="onBlur"
v-bind="$attrs" />
</div>
</template>
<script>
export default {
inheritAttrs: false,
props: {
type: {
type: String,
default: "text",
},
value: {
type: String,
default: "",
},
},
methods: {
onInput (e) {
this.$emit("input", e.target.value);
this.$parent.$emit("validate", e.target.value);
},
onBlur(){
this.$parent.$emit("validate", this.value);
}
},
};
</script>
dispatch事件派发
input组件触发my-form-item 的校验方法同样也会遇到form和item父子组件耦合的问题,他们的区别是form触发item组件校验事件是向下递归遍历寻找目标组件,触发目标事件,而input组件是向上寻找目标组件,触发目标事件。
继续回到emitter.js文件,实现dispatch方法
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root;
let name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
用dispath方法改写this.$parent,解决父子组件耦合问题
mixins: [emitter],
methods: {
onInput (e) {
this.$emit("input", e.target.value);
this.dispatch("my-form-item", "validate", e.target.value);
},
onBlur(){
this.dispatch("my-form-item", "validate", this.value);
}
},
毛坯房验收
我们已经实现了一个极简版的form组件,和input组件,相当于建成了一个毛坯房,是时候验收了
验收清单
-
事件广播和派发emitter.js -
my-form组件 -
my-form-item组件 -
my-input组件
验收效果 
|