Vue基礎(chǔ)popover彈出框編寫(xiě)及bug問(wèn)題分析
引言
最近做了一套Vue 的UI組件框架,里面牽涉到的popover組件個(gè)人覺(jué)得很有意思,也是個(gè)人覺(jué)得做出來(lái)最好看的一個(gè)組件。
首先新建一個(gè)Vue項(xiàng)目,無(wú)需贅述了。
制定結(jié)構(gòu)
給組件命名為bl-popover
<bl-popover> <template slot="content"> 這是內(nèi)容,這是內(nèi)容,這是內(nèi)容。 這是內(nèi)容,這是內(nèi)容,這是內(nèi)容。 </template> <button>點(diǎn)擊,顯示內(nèi)容</button> </bl-popover>
這種結(jié)構(gòu)也許不錯(cuò)。
content
的slot
包裹popover里需要顯示的內(nèi)容,而原始默認(rèn)slot
里包裹popover觸發(fā)器。
創(chuàng)建組件文件,實(shí)現(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)先寫(xiě)成這樣,符合我對(duì)使用結(jié)構(gòu)的印象,接著想要測(cè)試的話就注冊(cè)這個(gè)組件,也無(wú)需贅述了。
設(shè)置為display:inline-block
可不用占滿一整行。
我們需要用觸發(fā)器來(lái)顯示和隱藏popover,所以在data里設(shè)置一個(gè)show
屬性。
讓觸發(fā)器被點(diǎn)擊實(shí)現(xiàn)切換。但由于slot
標(biāo)簽是不能接受任何東西的,所以我們把事件綁定在整個(gè)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>
此時(shí)即可實(shí)現(xiàn)點(diǎn)擊button
就可顯示popover。
這時(shí)候我們需要做的就是將popover變?yōu)榻^對(duì)定位。
絕對(duì)定位
給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>
即可點(diǎn)擊之后顯示成這樣
如何點(diǎn)擊外部關(guān)閉
Bug:監(jiān)聽(tīng)body問(wèn)題。
name我該如何關(guān)閉這個(gè)popover呢?是點(diǎn)其他地方關(guān)閉嗎?
本想這樣處理:
methods:{ showChange(){ this.show = !this.show if(this.show===true){ document.body.addEventListener('click',()=>{ this.show = false }) } } }
但事實(shí)是,這樣連popover都無(wú)法打開(kāi)了。這是由于原生JS的事件冒泡機(jī)制。 this.show = !this.show
和 this.show = false
是在一次點(diǎn)擊下全部完成了,所以他就直接給關(guān)了,根本看不見(jiàn)。
故這里我們將他改為異步,就不會(huì)一口氣都走完了。
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'); }) }) } } } }
即可解決這個(gè)開(kāi)了就關(guān)的問(wèn)題。
但是還有其他的問(wèn)題。 實(shí)際上,body的大小只有藍(lán)色邊框內(nèi)的部分
也就是點(diǎn)擊藍(lán)色邊框之外的部分,是關(guān)不掉這個(gè)popover的。
所以不要監(jiān)聽(tīng)body,直接監(jiān)聽(tīng)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:再次打開(kāi)失敗。
解決了點(diǎn)擊外部失效的問(wèn)題,我發(fā)現(xiàn),點(diǎn)擊打開(kāi)popover,再點(diǎn)擊外部關(guān)閉,就無(wú)法再次打開(kāi)popover了。
這里來(lái)看控制臺(tái)。
第一次點(diǎn)擊觸發(fā)器
第二次點(diǎn)擊外部
第三次點(diǎn)擊觸發(fā)器
會(huì)發(fā)現(xiàn)第三次點(diǎn)擊直接走完了切換和關(guān)閉。
這是為什么呢,因?yàn)檫@時(shí)候有兩個(gè)事件監(jiān)聽(tīng)器在運(yùn)作,一個(gè)是popover上的,一個(gè)是document上的。順序是先調(diào)用popover上的,再調(diào)用document上的。
我們?cè)賮?lái)看看第四次點(diǎn)擊觸發(fā)器
再看看第五次,第六次
會(huì)發(fā)現(xiàn)關(guān)閉show出現(xiàn)越來(lái)越多次,這是為什么呢。這是因?yàn)槲覀凕c(diǎn)擊一次觸發(fā)器,執(zhí)行一次showChange
方法,就會(huì)在document上新增一個(gè)addEventListener
,而我們并沒(méi)有在時(shí)間結(jié)束之后刪除他,就越來(lái)越多,越來(lái)越多。
那么我們就需要在每次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)聽(tīng)器'); }.bind(this)) }) } } } }
這里我們需要removeEventListener
,所以監(jiān)聽(tīng)器需要有個(gè)函數(shù)名,我起名為listener
,但不是箭頭函數(shù)了,this.show
的this就不是指向Vue實(shí)例了,而是調(diào)用這個(gè)監(jiān)聽(tīng)器的document了。所以需要使用bind
把this綁定一下。
此時(shí)前兩次點(diǎn)擊是
但是第三次點(diǎn)擊
說(shuō)明還是有bug,看起來(lái)這個(gè)刪除監(jiān)聽(tīng)器根本就沒(méi)有成功。
這里的原因比較復(fù)雜。我為了讓listener內(nèi)的this還是指向Vue實(shí)例,使用了bind
,但其實(shí)使用了bind
之后的listener并不是原本的listener了,而是綁定后返回的一個(gè)新的函數(shù)。所以并沒(méi)有刪掉原本的listener。所以在這里要避免使用bind
。
methods:{ showChange(){ this.show = !this.show if(this.show===true) { console.log('切換show'); setTimeout(()=>{ let listener = () =>{ console.log('新增事件監(jiān)聽(tīng)器'); this.show = false console.log('關(guān)閉show'); document.removeEventListener('click',listener) console.log('刪除監(jiān)聽(tīng)器'); } document.addEventListener('click',listener) }) } } }
新建了一個(gè)箭頭函數(shù),就避免了使用bind
。
這是此時(shí)點(diǎn)擊了六次的結(jié)果,可以正常開(kāi)閉popover了。
Bug:點(diǎn)擊popover氣泡本身也會(huì)關(guān)閉popover
雖然開(kāi)閉正常了,但是點(diǎn)擊氣泡本身,我本身不希望他隱藏,可他還是關(guān)閉了。
這是因?yàn)槭录芭莸脑?,我們點(diǎn)擊popover或者觸發(fā)器,事件會(huì)冒泡到document上面去,還是會(huì)觸發(fā)。
我這時(shí)選擇了這個(gè)處理方法
<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)聽(tīng)器'); this.show = false console.log('關(guān)閉show'); document.removeEventListener('click',listener) console.log('刪除監(jiān)聽(tīng)器'); } document.addEventListener('click',listener) }) } } } } </script>
在可以被點(diǎn)擊的地方使用了.stop
阻止冒泡,可以發(fā)現(xiàn),點(diǎn)擊氣泡不會(huì)被關(guān)閉了,并且document上的事件監(jiān)聽(tīng)器也沒(méi)有產(chǎn)生或觸發(fā)。
這樣就實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單的popover。
其他Bug
Bug:外部有overflow:hidden,會(huì)遮擋popover。
我在popover組件的外部套一個(gè)div,設(shè)置overflow:hidden,會(huì)發(fā)生這樣的情況
會(huì)被擋住。
說(shuō)明這個(gè)問(wèn)題非常嚴(yán)重,代碼可能要全部砍倒重練。
而且單純地阻止冒泡也會(huì)帶來(lái)很多問(wèn)題,會(huì)打斷用戶的事件鏈。
那我選擇讓這個(gè)彈出框氣泡移到body上,就可以避免這個(gè)問(wèn)題。
<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)聽(tīng)器'); this.show = false console.log('關(guān)閉show'); document.removeEventListener('click',listener) console.log('刪除監(jiān)聽(tīng)器'); } document.addEventListener('click',listener) }) } } }, mounted() { document.body.appendChild(this.$refs.contentWrapper) } } </script>
可以看到,為了能讓v-if===false
的情況下,也能檢查的到contentWrapper,我把v-if
換成了v-show
,因?yàn)?code>v-show只是切換display:none
,影響的是元素的顯示隱藏。而v-if
影響的是元素是否被render到DOM樹(shù)上。
但是使用v-show
就會(huì)讓contentWrapper一開(kāi)始就存在在頁(yè)面上,我并不想這樣。
<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)聽(tīng)器'); this.show = false console.log('關(guān)閉show'); document.removeEventListener('click',listener) console.log('刪除監(jiān)聽(tīng)器'); } document.addEventListener('click',listener) }) } } }, } </script>
這樣,讓我點(diǎn)擊觸發(fā)器的時(shí)候,再將彈出框移動(dòng)到body上,也可以。記住這一步要放在nextTick
里,不然還是可能找不到contentWrapper。
這時(shí)候我要想辦法讓這個(gè)彈出框像以前一樣顯示。首先要找到觸發(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在一個(gè)正確的位置了。
<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移出來(lái),因?yàn)?content-wrapper已經(jīng)不在.popover里了,而我們還在使用scoped。
Bug:位置其實(shí)不正確
我們?cè)谡麄€(gè)頁(yè)面上端再加一個(gè)div試試
會(huì)發(fā)現(xiàn)位置根本不對(duì)。
這是因?yàn)椋^對(duì)定位并未根據(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會(huì)打斷事件鏈
<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('打開(kāi)'); 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>
讓我們來(lái)判斷你點(diǎn)擊的是什么,然后再?zèng)Q定要做什么,就可以避免這個(gè)問(wèn)題了。
Bug:如果只點(diǎn)擊觸發(fā)器,會(huì)進(jìn)行重復(fù)監(jiān)聽(tīng)。
只點(diǎn)擊觸發(fā)器,會(huì)重復(fù)監(jiān)聽(tīng),而且不會(huì)實(shí)行removeEventListener。
那我們把document.addEventListener
放在created里是不是會(huì)方便很多呢?
確實(shí)會(huì)方便很多,但是,如果我頁(yè)面上有100個(gè)這個(gè)組件,那我打開(kāi)頁(yè)面就要加100個(gè)監(jiān)聽(tīng)器,那完蛋了。所以不可以這樣。
這個(gè)時(shí)候我要進(jìn)行的是一個(gè)高內(nèi)聚的設(shè)計(jì)模式。
將close和open抽離出來(lái),大家都用這兩個(gè)控制開(kāi)閉。
<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
兩個(gè)方法聚合了所有和開(kāi)閉彈出層有關(guān)的東西。
這樣就完成了一個(gè)基礎(chǔ)的,可點(diǎn)擊在上方出現(xiàn)的popover。
剩下的,Hover觸發(fā),四個(gè)方向觸發(fā),具體樣式,也是依葫蘆畫(huà)瓢,這里就不多贅述了。
以上就是Vue基礎(chǔ)popover彈出框編寫(xiě)及bug問(wèn)題分析的詳細(xì)內(nèi)容,更多關(guān)于Vue popover彈出框的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue3?el-table結(jié)合seamless-scroll實(shí)現(xiàn)表格數(shù)據(jù)滾動(dòng)的思路詳解
這篇文章主要介紹了vue3?el-table結(jié)合seamless-scroll實(shí)現(xiàn)表格數(shù)據(jù)滾動(dòng),創(chuàng)建兩個(gè)table,隱藏第一個(gè)table的body部分,這樣就能得到一個(gè)固定的head,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07vue iview組件表格 render函數(shù)的使用方法詳解
下面小編就為大家分享一篇vue iview組件表格 render函數(shù)的使用方法詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03vue學(xué)習(xí)之Vue-Router用法實(shí)例分析
這篇文章主要介紹了vue學(xué)習(xí)之Vue-Router用法,結(jié)合實(shí)例形式分析了Vue-Router路由原理與常見(jiàn)操作技巧,需要的朋友可以參考下2020-01-01實(shí)例詳解Vue項(xiàng)目使用eslint + prettier規(guī)范代碼風(fēng)格
這篇文章主要介紹了Vue項(xiàng)目使用eslint + prettier規(guī)范代碼風(fēng)格,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2018-08-08vue-cli2與vue-cli3在一臺(tái)電腦共存的實(shí)現(xiàn)方法
這篇文章主要介紹了vue-cli2與vue-cli3在一臺(tái)電腦共存的實(shí)現(xiàn)方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-09-09vue簡(jiǎn)單實(shí)現(xiàn)轉(zhuǎn)盤(pán)抽獎(jiǎng)
這篇文章主要為大家詳細(xì)介紹了vue簡(jiǎn)單實(shí)現(xiàn)轉(zhuǎn)盤(pán)抽獎(jiǎng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-03-03關(guān)于引入vue.js 文件的知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家分享的是關(guān)于引入vue.js 文件的知識(shí)點(diǎn)總結(jié),有需要的朋友們可以參考學(xué)習(xí)下。2020-01-01