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{
width: 100%;
height: 100%;
position: relative;
cursor: pointer;
}
.input{
width: 100%;
height: 100%;
position: relative;
cursor: pointer;
}
.input>input{
width: 100%;
height: 100%;
cursor: pointer;
}
.input>img{
right: 0;
top: 50%;
width: 12px;
height: 12px;
position: absolute;
transform: translateY(-50%);
}
.content{
width: 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;
width: 100%;
@include padding(0 0 0 10px);
}
.content>li:hover{
color: #409eff;
background-color: rgba(33,33,33,.2);
}
</style>
