通用表单组件 收集数据、校验数据并提交。
实现以下组件
组件设计
原则是高内聚,低耦合; 组件的功能高度专一,这样可以使得组件使用变得灵活,组件之间的耦合度也会随之减小,从而可以提高组件的复用性。
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
至此组件之间的解耦成功