Vue基礎(chǔ)popover彈出框編寫及bug問題分析
引言
最近做了一套Vue 的UI組件框架,里面牽涉到的popover組件個人覺得很有意思,也是個人覺得做出來最好看的一個組件。

首先新建一個Vue項目,無需贅述了。
制定結(jié)構(gòu)
給組件命名為bl-popover
<bl-popover>
<template slot="content">
這是內(nèi)容,這是內(nèi)容,這是內(nèi)容。
這是內(nèi)容,這是內(nèi)容,這是內(nèi)容。
</template>
<button>點擊,顯示內(nèi)容</button>
</bl-popover>
這種結(jié)構(gòu)也許不錯。
content的slot包裹popover里需要顯示的內(nèi)容,而原始默認(rèn)slot里包裹popover觸發(fā)器。
創(chuàng)建組件文件,實現(xiàn)基本功能
src目錄創(chuàng)建popover.vue文件。
<template>
<div class="popover">
<slotname="content"></slot>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block
}
</style>
文件內(nèi)部結(jié)構(gòu)先寫成這樣,符合我對使用結(jié)構(gòu)的印象,接著想要測試的話就注冊這個組件,也無需贅述了。
設(shè)置為display:inline-block可不用占滿一整行。
我們需要用觸發(fā)器來顯示和隱藏popover,所以在data里設(shè)置一個show屬性。
讓觸發(fā)器被點擊實現(xiàn)切換。但由于slot標(biāo)簽是不能接受任何東西的,所以我們把事件綁定在整個div上。
就變成了
<template>
<div class="popover" @click="showChange">
<slotname="content" v-if="show"></slot>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
}
}
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block
}
</style>
此時即可實現(xiàn)點擊button就可顯示popover。
這時候我們需要做的就是將popover變?yōu)榻^對定位。
絕對定位
給slot標(biāo)簽外包裹標(biāo)簽即可選中slot。
<template>
<div class="popover" @click="showChange">
<div class="content-wrapper" v-if="show">
<slotname="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
}
}
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block;
position: relative;
.content-wrapper{
position:absolute;
bottom:100%;
left:0;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
}
}
</style>
即可點擊之后顯示成這樣

如何點擊外部關(guān)閉
Bug:監(jiān)聽body問題。
name我該如何關(guān)閉這個popover呢?是點其他地方關(guān)閉嗎?
本想這樣處理:
methods:{
showChange(){
this.show = !this.show
if(this.show===true){
document.body.addEventListener('click',()=>{
this.show = false
})
}
}
}
但事實是,這樣連popover都無法打開了。這是由于原生JS的事件冒泡機(jī)制。 this.show = !this.show和 this.show = false是在一次點擊下全部完成了,所以他就直接給關(guān)了,根本看不見。
故這里我們將他改為異步,就不會一口氣都走完了。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
setTimeout(()=>{
document.body.addEventListener('click', () => {
this.show = false
console.log('關(guān)閉show');
})
})
}
}
}
}
即可解決這個開了就關(guān)的問題。
但是還有其他的問題。 實際上,body的大小只有藍(lán)色邊框內(nèi)的部分

也就是點擊藍(lán)色邊框之外的部分,是關(guān)不掉這個popover的。
所以不要監(jiān)聽body,直接監(jiān)聽document就好。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
setTimeout(()=>{
document.addEventListener('click', () => {
this.show = false
console.log('關(guān)閉show');
})
})
}
}
}
}
Bug:再次打開失敗。
解決了點擊外部失效的問題,我發(fā)現(xiàn),點擊打開popover,再點擊外部關(guān)閉,就無法再次打開popover了。
這里來看控制臺。
第一次點擊觸發(fā)器

第二次點擊外部

第三次點擊觸發(fā)器

會發(fā)現(xiàn)第三次點擊直接走完了切換和關(guān)閉。
這是為什么呢,因為這時候有兩個事件監(jiān)聽器在運作,一個是popover上的,一個是document上的。順序是先調(diào)用popover上的,再調(diào)用document上的。
我們再來看看第四次點擊觸發(fā)器

再看看第五次,第六次

會發(fā)現(xiàn)關(guān)閉show出現(xiàn)越來越多次,這是為什么呢。這是因為我們點擊一次觸發(fā)器,執(zhí)行一次showChange方法,就會在document上新增一個addEventListener,而我們并沒有在時間結(jié)束之后刪除他,就越來越多,越來越多。
那么我們就需要在每次popover關(guān)閉之后,刪除他。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
setTimeout(()=>{
document.addEventListener('click', function listener{
this.show = false
console.log('關(guān)閉show');
document.removeEventListener('click',listener)
console.log('刪除監(jiān)聽器');
}.bind(this))
})
}
}
}
}
這里我們需要removeEventListener,所以監(jiān)聽器需要有個函數(shù)名,我起名為listener,但不是箭頭函數(shù)了,this.show的this就不是指向Vue實例了,而是調(diào)用這個監(jiān)聽器的document了。所以需要使用bind把this綁定一下。
此時前兩次點擊是

但是第三次點擊

說明還是有bug,看起來這個刪除監(jiān)聽器根本就沒有成功。
這里的原因比較復(fù)雜。我為了讓listener內(nèi)的this還是指向Vue實例,使用了bind,但其實使用了bind之后的listener并不是原本的listener了,而是綁定后返回的一個新的函數(shù)。所以并沒有刪掉原本的listener。所以在這里要避免使用bind。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
setTimeout(()=>{
let listener = () =>{
console.log('新增事件監(jiān)聽器');
this.show = false
console.log('關(guān)閉show');
document.removeEventListener('click',listener)
console.log('刪除監(jiān)聽器');
}
document.addEventListener('click',listener)
})
}
}
}
新建了一個箭頭函數(shù),就避免了使用bind。
這是此時點擊了六次的結(jié)果,可以正常開閉popover了。

Bug:點擊popover氣泡本身也會關(guān)閉popover
雖然開閉正常了,但是點擊氣泡本身,我本身不希望他隱藏,可他還是關(guān)閉了。
這是因為事件冒泡的原因,我們點擊popover或者觸發(fā)器,事件會冒泡到document上面去,還是會觸發(fā)。
我這時選擇了這個處理方法
<template>
<div class="popover" @click.stop="showChange">
<div class="content-wrapper" v-if="show" @click.stop>
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
this.$nextTick(()=>{
let listener = () =>{
console.log('新增事件監(jiān)聽器');
this.show = false
console.log('關(guān)閉show');
document.removeEventListener('click',listener)
console.log('刪除監(jiān)聽器');
}
document.addEventListener('click',listener)
})
}
}
}
}
</script>
在可以被點擊的地方使用了.stop阻止冒泡,可以發(fā)現(xiàn),點擊氣泡不會被關(guān)閉了,并且document上的事件監(jiān)聽器也沒有產(chǎn)生或觸發(fā)。

這樣就實現(xiàn)了一個最簡單的popover。
其他Bug
Bug:外部有overflow:hidden,會遮擋popover。
我在popover組件的外部套一個div,設(shè)置overflow:hidden,會發(fā)生這樣的情況

會被擋住。
說明這個問題非常嚴(yán)重,代碼可能要全部砍倒重練。
而且單純地阻止冒泡也會帶來很多問題,會打斷用戶的事件鏈。
那我選擇讓這個彈出框氣泡移到body上,就可以避免這個問題。
<template>
<div class="popover" @click.stop="showChange">
<div ref="contentWrapper" class="content-wrapper" v-show="show" @click.stop>
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
this.$nextTick(()=>{
let listener = () =>{
console.log('新增事件監(jiān)聽器');
this.show = false
console.log('關(guān)閉show');
document.removeEventListener('click',listener)
console.log('刪除監(jiān)聽器');
}
document.addEventListener('click',listener)
})
}
}
},
mounted() {
document.body.appendChild(this.$refs.contentWrapper)
}
}
</script>

可以看到,為了能讓v-if===false的情況下,也能檢查的到contentWrapper,我把v-if換成了v-show,因為v-show只是切換display:none,影響的是元素的顯示隱藏。而v-if影響的是元素是否被render到DOM樹上。
但是使用v-show就會讓contentWrapper一開始就存在在頁面上,我并不想這樣。
<template>
<div class="popover" @click.stop="showChange">
<div ref="contentWrapper" class="content-wrapper" v-if="show" @click.stop>
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
console.log('切換show');
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let listener = () =>{
console.log('新增事件監(jiān)聽器');
this.show = false
console.log('關(guān)閉show');
document.removeEventListener('click',listener)
console.log('刪除監(jiān)聽器');
}
document.addEventListener('click',listener)
})
}
}
},
}
</script>
這樣,讓我點擊觸發(fā)器的時候,再將彈出框移動到body上,也可以。記住這一步要放在nextTick里,不然還是可能找不到contentWrapper。
這時候我要想辦法讓這個彈出框像以前一樣顯示。首先要找到觸發(fā)器的位置。
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let{left,top,width,height} = this.$refs.triggerWrapper.getBoundingClientRect()
console.log(left, top, width, height);
let listener = () =>{
this.show = false
document.removeEventListener('click',listener)
}
document.addEventListener('click',listener)
})
}
}
},

這樣就可以讓contentWrapper在一個正確的位置了。
<template>
<div class="popover" @click.stop="showChange">
<div ref="contentWrapper" class="content-wrapper" v-if="show" @click.stop>
<slot name="content"></slot>
</div>
<span ref="triggerWrapper">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'Popover',
data(){
return{
show:false
}
},
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let{left,top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top +'px'
this.$refs.contentWrapper.style.left = left + 'px'
let listener = () =>{
this.show = false
document.removeEventListener('click',listener)
}
document.addEventListener('click',listener)
})
}
}
},
}
</script>
<style lang="scss" scoped>
.popover{
display:inline-block;
position: relative;
}
.content-wrapper{
position:absolute;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
transform: translateY(-100%);
}
</style>
注意要把.content-wrapper移出來,因為.content-wrapper已經(jīng)不在.popover里了,而我們還在使用scoped。

Bug:位置其實不正確
我們在整個頁面上端再加一個div試試

會發(fā)現(xiàn)位置根本不對。
這是因為,絕對定位并未根據(jù)觸發(fā)器為基準(zhǔn),而是根據(jù)他的父元素body元素為基準(zhǔn)的。
而body頂部到視窗的差值,就是scrollY
橫向也一樣的道理。 所以改成
methods:{
showChange(){
this.show = !this.show
if(this.show===true) {
this.$nextTick(()=>{
document.body.appendChild(this.$refs.contentWrapper)
let{left,top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top +scrollY +'px'
this.$refs.contentWrapper.style.left = left + scrollX + 'px'
let listener = () =>{
this.show = false
document.removeEventListener('click',listener)
}
document.addEventListener('click',listener)
})
}
}
},

就正常了。
Bug:.stop會打斷事件鏈
<template>
<div class="popover" @click="showChange">
<div v-if="show" ref="contentWrapper" class="content-wrapper">
<slot name="content"></slot>
</div>
<span ref="triggerWrapper">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'Popover',
data() {
return {
show: false
}
},
methods: {
position() {
document.body.appendChild(this.$refs.contentWrapper)
let {left, top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top + scrollY + 'px'
this.$refs.contentWrapper.style.left = left + scrollX + 'px'
},
eventListener() {
let listener = (event) => {
if (!this.$refs.contentWrapper.contains(event.target)) {
this.show = false
document.removeEventListener('click', listener)
}
}
document.addEventListener('click', listener)
},
showChange(event) {
if (this.$refs.triggerWrapper.contains(event.target)) {
this.show = !this.show
console.log('打開');
if (this.show === true) {
this.$nextTick(() => {
this.position()
this.eventListener()
})
}
}
}
},
}
</script>
<style lang="scss" scoped>
.popover {
display: inline-block;
position: relative;
}
.content-wrapper {
position: absolute;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
transform: translateY(-100%);
}
</style>
讓我們來判斷你點擊的是什么,然后再決定要做什么,就可以避免這個問題了。
Bug:如果只點擊觸發(fā)器,會進(jìn)行重復(fù)監(jiān)聽。
只點擊觸發(fā)器,會重復(fù)監(jiān)聽,而且不會實行removeEventListener。
那我們把document.addEventListener放在created里是不是會方便很多呢?
確實會方便很多,但是,如果我頁面上有100個這個組件,那我打開頁面就要加100個監(jiān)聽器,那完蛋了。所以不可以這樣。
這個時候我要進(jìn)行的是一個高內(nèi)聚的設(shè)計模式。
將close和open抽離出來,大家都用這兩個控制開閉。
<template>
<div ref="popover" class="popover" @click="showChange">
<div v-if="show" ref="contentWrapper" class="content-wrapper">
<slot name="content"></slot>
</div>
<span ref="triggerWrapper">
<slot></slot>
</span>
</div>
</template>
<script>
export default {
name: 'Popover',
data() {
return {
show: false
}
},
methods: {
position() {
document.body.appendChild(this.$refs.contentWrapper)
let {left, top} = this.$refs.triggerWrapper.getBoundingClientRect()
this.$refs.contentWrapper.style.top = top + scrollY + 'px'
this.$refs.contentWrapper.style.left = left + scrollX + 'px'
},
listener(event) {
if (this.$refs.popover &&
this.$refs.popover.contains(event.target) || this.$refs.popover === event.target) {
return;
}
if (this.$refs.contentWrapper &&
this.$refs.contentWrapper.contains(event.target) || this.$refs.contentWrapper === event.target) {
return;
}
this.close()
},
close() {
this.show = false
document.removeEventListener('click', this.listener)
},
open() {
this.show = true
this.$nextTick(() => {
this.position()
document.addEventListener('click', this.listener)
})
},
showChange(event) {
if (this.$refs.triggerWrapper.contains(event.target)) {
if (this.show === true) {
this.close()
} else {
this.open()
}
}
}
},
}
</script>
<style lang="scss" scoped>
.popover {
display: inline-block;
position: relative;
}
.content-wrapper {
position: absolute;
padding: 6px;
border: 1px solid #ebeef5;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
background: white;
transform: translateY(-100%);
}
</style>
這樣高聚合了之后,將close和open兩個方法聚合了所有和開閉彈出層有關(guān)的東西。
這樣就完成了一個基礎(chǔ)的,可點擊在上方出現(xiàn)的popover。
剩下的,Hover觸發(fā),四個方向觸發(fā),具體樣式,也是依葫蘆畫瓢,這里就不多贅述了。
以上就是Vue基礎(chǔ)popover彈出框編寫及bug問題分析的詳細(xì)內(nèi)容,更多關(guān)于Vue popover彈出框的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文搞懂Vue3中的異步組件defineAsyncComponentAPI的用法
這篇文章主要介紹了一文搞懂Vue3中的異步組件,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的小伙伴可以參考一下2022-07-07
使用element-ui的Pagination分頁的注意事項及說明
這篇文章主要介紹了使用element-ui的Pagination分頁的注意事項及說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-02-02
vue3.0 CLI - 2.2 - 組件 home.vue 的初步改造
這篇文章主要介紹了vue3.0 CLI - 2.2 - 組件 home.vue 的初步改造,home.vue 組件有了兩個屬性:navs 和 tts 屬性,具體實例代碼大家跟隨小編一起通過本文學(xué)習(xí)吧2018-09-09
使用websocket和Vue2中的props實時更新數(shù)據(jù)方式
這篇文章主要介紹了使用websocket和Vue2中的props實時更新數(shù)據(jù)方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-08-08

