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)容,而原始默認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
標簽是不能接受任何東西的,所以我們把事件綁定在整個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
標簽外包裹標簽即可選中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的事件冒泡機制。 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的大小只有藍色邊框內(nèi)的部分
也就是點擊藍色邊框之外的部分,是關(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)聽器根本就沒有成功。
這里的原因比較復雜。我為了讓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ā)生這樣的情況
會被擋住。
說明這個問題非常嚴重,代碼可能要全部砍倒重練。
而且單純地阻止冒泡也會帶來很多問題,會打斷用戶的事件鏈。
那我選擇讓這個彈出框氣泡移到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ā)器為基準,而是根據(jù)他的父元素body元素為基準的。
而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ā)器,會進行重復監(jiān)聽。
只點擊觸發(fā)器,會重復監(jiān)聽,而且不會實行removeEventListener。
那我們把document.addEventListener
放在created里是不是會方便很多呢?
確實會方便很多,但是,如果我頁面上有100個這個組件,那我打開頁面就要加100個監(jiā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問題分析的詳細內(nèi)容,更多關(guān)于Vue popover彈出框的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue3?el-table結(jié)合seamless-scroll實現(xiàn)表格數(shù)據(jù)滾動的思路詳解
這篇文章主要介紹了vue3?el-table結(jié)合seamless-scroll實現(xiàn)表格數(shù)據(jù)滾動,創(chuàng)建兩個table,隱藏第一個table的body部分,這樣就能得到一個固定的head,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07vue iview組件表格 render函數(shù)的使用方法詳解
下面小編就為大家分享一篇vue iview組件表格 render函數(shù)的使用方法詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-03-03實例詳解Vue項目使用eslint + prettier規(guī)范代碼風格
這篇文章主要介紹了Vue項目使用eslint + prettier規(guī)范代碼風格,本文通過實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2018-08-08vue-cli2與vue-cli3在一臺電腦共存的實現(xiàn)方法
這篇文章主要介紹了vue-cli2與vue-cli3在一臺電腦共存的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2019-09-09關(guān)于引入vue.js 文件的知識點總結(jié)
在本篇文章里小編給大家分享的是關(guān)于引入vue.js 文件的知識點總結(jié),有需要的朋友們可以參考學習下。2020-01-01