1.目的
看了很多element-ui的源码,决定自己实现一个简单的select组件,遇到的几个难点,便记录下来.
2.难点一
element-ui中的select组件通过v-model可以绑定数据,但在我平时用v-model只是在input中使用过,只知道v-model可以双向绑定数据,但并不清楚其中的实现过程,所以 需要清晰的了解v-model是什么,如下.
<input v-model="test"/> <input :value="test" @input="test = $event.target.value"/> // 第一行和第二行的性质是一样的,v-model是一个vue的语法糖
以上是input输入框中的v-model,input标签在输入的时候默认会触发'input'事件, 但是自定义的组件并不会,所以需要我们自己手动发送一个'input'事件,其次是,使用了v-model指令以后会默认动态绑定一个属性值value,因此我们在自定义组件中可以在props接收value,并绑定到组件当中,从而实现了双向绑定,具体可以看参考完整代码.
3.难点二
当select组件显示选择框时,合理的逻辑是是点击空白或者点击自身都要将选择框关闭, 起初实现是在document中绑定一个click事件用于关闭选择框,当然select点击得阻止事件冒泡,这样的实现方式是在一个页面只有一个select组件是没有问题的,但是当出现多个select组件就会出现一个bug,点击完一个select以后点击另外一个是无法关闭前一个select框的选择框的,问题出在因为每个select框都被阻止了事件的冒泡,自然不会触发document的click事件,从而无法关闭,知晓原因,解决方案如下:
// 显示选择框 showSel(){ this.show = true; addEvent(document, 'click',this.hideSel, true); } // 隐藏选择框 hideSel(e){ this.show = false;
// 如果是子元素,则阻止事件捕获 if(this.$refs.sel && this.$refs.sel.contains(e.target)){ stopEvent(e); } removeEvent(document,'click',this.hideSel,true); } // 显示或隐藏 toggle(){ this.show && this.hideSel() || this.showSel(); } // 注意:其中addEvent,removeEvent,stopEvent是为了兼容处理而自定义的方法
以上就是这次编写select组件的所得,附上完整实例代码.
<template> <div class="select" @click="toggle" ref="sel"> <div class="input"> <input type="text" :placeholder="placeholder" readonly :value = 'value' @blur="handle"> <img src="../images/drop.svg"> </div> <ul class="content" :class="{'bottom' : position == 'bottom', 'top' : position == 'top'}" v-show="show && values.length" ref="content"> <li v-for="item in values">{{item}}</li> </ul> </div> </template> <script> import { addEvent, removeEvent, stopEvent } from '../service/utli.js'; export default { name : 'comSelect', data(){ return{ val : '', show : false, position : 'bottom' } }, props : { values : { type : Array, default(){ return [] } }, value : { }, placeholder:{ type : String, default : '请选择' }, }, mounted(){ this.computePos(); }, methods:{ getElementTop(element){ var actualTop = element.offsetTop; var current = element.offsetParent; while (current !== null){ actualTop += current.offsetTop; current = current.offsetParent; } return actualTop; }, // 计算选择框是往上弹出还是往下弹出 computePos(){ let elHeight = this.$refs.sel.offsetHeight; let absPos = this.getElementTop(this.$refs.sel); let contentHeight = this.values.length*40; let docScrollHei = document.body.scrollTop || document.documentElement.scrollTop || 0; let docHeight = document.documentElement.clientHeight || document.body.clientHeight || 0; if((elHeight+absPos+contentHeight-docScrollHei)>docHeight){ this.position = 'top'; }else{ this.position = 'bottom'; } }, setVal(item){ this.$emit('input',item); }, handle(){ this.$emit('blur'); }, showSel(){ this.show = true; addEvent(document, 'click',this.hideSel, true); }, hideSel(e){ this.show = false; console.log(this.$refs.sel.contains(e.target)); if(this.$refs.sel && this.$refs.sel.contains(e.target)){ // 如果是子元素则阻止事件捕获 stopEvent(e);
this.setVal(e.target.innerHtml); } removeEvent(document,'click',this.hideSel,true); }, toggle(){ this.show && this.hideSel() || this.showSel(); } } } </script> <style scoped lang="scss"> @import '../style/mixin.scss'; .select{ 100%; height: 100%; position: relative; cursor: pointer; } .input{ 100%; height: 100%; position: relative; cursor: pointer; } .input>input{ 100%; height: 100%; cursor: pointer; } .input>img{ right: 0; top: 50%; 12px; height: 12px; position: absolute; transform: translateY(-50%); } .content{ 100%; max-height: px(300); overflow-y: scroll; border-radius: 10px; @include padding(4px 0); position: absolute; left: 0; background-color: white; box-shadow: 0 0 20px 2px #ccc; @include prix(transform, translateY(5px)); z-index: 2; } .content::-webkit-scrollbar {display: none;} .bottom{ top: 100%; } .top{ bottom: 125%; } .content>li{ height: 40px; line-height: 40px; 100%; @include padding(0 0 0 10px); } .content>li:hover{ color: #409eff; background-color: rgba(33,33,33,.2); } </style>