通用表单组件 收集数据、校验数据并提交。
实现以下组件

组件设计
原则是高内聚,低耦合; 组件的功能高度专一,这样可以使得组件使用变得灵活,组件之间的耦合度也会随之减小,从而可以提高组件的复用性。

Input.vue 组件
对着elmentui 简单的实现了一些功能,此段代码有一个问题是 this.$parent.$emit('validate') 触发校验是,this.$parent 会增加耦合度。
<template>
<div class="input-box">
<!-- 前置内容 -->
<input
:value="value"
@input="handleInput"
v-bind="$attrs"
class="minput"
:class="[
inputSize() ? 'm-input--' + inputSize() : ''
]"/>
<!-- 后置内容 -->
<span v-if="clearable" @click="clear">x</span>
</div>
</template>
<script>
import emitter from '@/mixins/emitter.js'
export default {
inheritAttrs: false, // 此处设置禁用继承特性
mixins: [ emitter ],
props: {
value: { // 值
type: String,
default: ''
},
clearable: { // 清空文本域
type: Boolean,
default: false
},
size: { // 字体大小 big mini
type: String,
default: ''
},
validateEvent: { // oninput 是否触发校验
type: Boolean,
default: true
}
},
methods: {
handleInput(e) {
// 派发一个 input 事件
this.$emit('input', e.target.value)
// 执行校验
this.$parent.$emit('validate')
},
clear() {
this.$emit('input', '')
},
inputSize() { // input 大小
return this.size
}
}
};
</script>
<style scoped>
.input-box {
width: 180px;position: relative;
}
/* default */
.minput {
box-sizing: border-box;
width: 100%;
height: 35px;
line-height: 35px;
padding: 5px 15px;
color: #666;
font-size: 13px;
}
span {
display: inline-block;
position:absolute;
right: 7px;
top: 50%;
transform: translateY(-50%);
width: 10px;
height:10px;
cursor:pointer;
border: 1px solid #999;
border-radius: 50%;
font-size:12px;
line-height: 0.9;
}
.m-input--big {
height: 50px;
line-height: 50px;
}
.m-input--mini {
height: 30px;
line-height: 30px;
}
</style>
FormItem.vue 组件
<template>
<div class="form-item" prop:="name">
<label v-if="lable">{{ lable }}</label>
<slot></slot>
<div v-if="isErr" class="errInfo">{{ isErr }}</div>
</div>
</template>
<script>
export default {
inject: ['mForm'],
props: {
lable: {
type: String,
default: ''
},
prop: String
},
data() {
return {
isErr:''
};
},
mounted() {
this.$on('validate', this.validate)
},
methods: {
validate() { // 规则校验
// 规则
const rules = this.mForm.rules[this.prop]
// input 值
const value = this.mForm.model[this.prop]
// 校验
rules.forEach(rule => {
if(rule.required) { // 不为空
this.isErr = value === '' ? rule.message : ''
}
})
if(!this.isErr) return true
}
}
};
</script>
<style scoped>
.form-item{
display:flex;
width: 400px;
line-height: 35px;
}
.errInfo { color: brown}
</style>
Form.vue 组件
为了让所有子组件都能访问到自身属性这里 inject了自己。
这里有个问题是 校验所有规则的时候 使用了 this.$children, 会增加组件之间的耦合度。
<template>
<form>
<slot></slot>
</form>
</template>
<script>
export default {
name: 'myForm',
provide() {
return {
mForm: this
}
},
props: {
rules: {
type: Object
},
model: {
type: Object
}
},
data() {
return {
};
},
methods: {
validate(callback) {
const task = this.$children
.filter(item => item.prop)
.map(item => item.validate())
.every(item => item)
callback(task) // 执行回调
}
}
};
</script>
View.vue 组件
这里 this.$alert 弹窗组件下一小节再说
<template>
<div>
<Form
:model="userInfo" :rules="rules" ref="loginForm">
<FormItem lable="用户名:" prop="name" ref="formItem">
<Minput
v-model="userInfo.name"
type="textarea"
name="hong"
size="big"
clearable
maxlength="10"
placeholder="请输入帐号"/>
</FormItem>
<FormItem lable="密码:" prop="pwd">
<Minput
v-model="userInfo.pwd"
type="password"
maxlength="10"
placeholder="请输入密码"/>
</FormItem>
<FormItem><button @click.prevent="submit">登录</button></FormItem>
</Form>
</div>
</template>
<script>
import Minput from './src/Minput.vue'
import FormItem from './src/FormItem.vue'
import Form from './src/Form.vue'
export default {
data() {
return {
userInfo: {
name: '123',
pwd: '123'
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' }
],
pwd: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
};
},
components: { Minput, FormItem, Form },
methods: {
submit() {
this.$refs['loginForm'].validate((valid) => {
if(valid) {
this.$alert({
title: '系统消息',
content: '成功登录,哈喽,xxx',
duration: 2500,
showClose: true
})
}
})
}
}
};
</script>
<style scoped>
</style>
下面解决上面提到的两个耦合度的问题
在 elmentui 源码中发现它有更好的解决方法, 使用了 emitter.js 以混入(mixins)的方式
eimtter.js
里面有两个方法 一个 dispath 向上遍历父级组件,由于父级组件是单链的形式向上遍历,所以这种形式性能消耗比较小。
此方法主要是传入了组件名,和事件名,当遍历到父组件有此组件名则会,保存此父组件,让该父组件向自身派发事件
parent.$emit.apply(parent, [eventName, ...params])
/** * @description 向上遍历 父组件 并派发事件 * @param { String } componentName 组件名 * @param { String } eventName 派发事件名 * @param { Array } params 参数 */ dispath(componentName, eventName, params) { var parent = this.$parent || this.$root var 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, ...params]) },
第二个方法是广播 broadcast
该方法是向下以树的形式遍历,消耗性能较大。实现事件派发的形式和 dispath 差不多,都是自己给自己派发事件
// 注意: 遍历组件,会有性能消耗 // 广播 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: { /** * @description 向上遍历 父组件 并派发事件 * @param { String } componentName 组件名 * @param { String } eventName 派发事件名 * @param { Array } params 参数 */ dispath(componentName, eventName, params) { var parent = this.$parent || this.$root var 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, ...params]) }, // 向下派发 broadcast(componentName, eventName, params) { broadcast.call(this, componentName, eventName, params) } } }
解决 Input 的耦合
dispah 遍历父组件

解决 form 的耦合
由于广播会使得性能消耗较大,这里就不使用这种方法了。
当点击提交form 表单事件时,form 表单会校验所有需要校验的 form-item,需要校验的 form-item 会传入 prop 校验名称,因此之前使用 this.$children 的方法获取所有的 form-item 是否有 prop。
现在为了解耦,改变了思路,当 form-item 注册的时候向调用 dispath 派发事件,其实是父级给自己传事件,并传入自身(子组件)
form-item.vue

from.vue

至此组件之间的解耦成功
浙公网安备 33010602011771号