组件中设置body_Vue造轮子笔记4-popover组件的实现

cf1669640d1cf94565ece7cb7c5a1848.png

实现popover组件过程中遇到了很多的坑,所以单独列出来总结一下

实现组件四步走

需求分析

弹出框的出现形式,可以是点击出现,hover出现。

弹出框的显示位置,可以是上下左右四个方向显示,如果还想扩充,可以无限扩充位置

弹出框的内容,通过参数控制内容,内容是否可以满足html标签

弹出框如何关闭,点击外部body关闭,点击按钮关闭,点击弹出框内的按钮关闭

由此可见,一个小小的弹出框,需要实现如此多的需求,这个组件很难

状态分析,这个组件没分析出有什么状态

UI设计

e0a39edf37b47ca2262a69778c3bcfe0.png

其他位置靠脑补

代码实现

用户如何使用?

     <xxx-popover>
        <template slot="content">
            <div>popover内容</div>
        </template>
        <template slot="trigger">
            <xxx-button>click me</xxx-button>
        </template>
    </xxx-popover>

用户需要用具名插槽将内容和按钮都包裹起来

首先实现一个最基本的,我们点击按钮显示这个content

html结构

<div class="popover"@click="xxx">
    <div class="content" v-if="visible">
      <slot name="content"></slot>
    </div>
    <div class="trigger">
      <slot name="trigger"></slot>
    </div>
  </div>
methods:{
   showContent(){
      this.visible=!this.visible
    }
}

这个需求很简单

如何点击其他位置控制显隐呢

设想,点击body控制this.visible

showContent(){
      this.visible=!this.visible
      console.log('显示了')
      if(this.visible){
        document.body.addEventListener('click',()=>{
          this.visible=false
          console.log('关闭了')
        })
      }
    }

然后发现点击毫无反应

通过打log发现

5bb90fd055644ef6ad31dfed7d210a37.png

点击一次按钮,同时触发显示和关闭

然后将关闭事件变为延时操作

if(this.visible){
          setTimeout(()=>{
            document.body.addEventListener('click',()=>{
              this.visible=false
              console.log('关闭了')
            })
          },1000)
      }

1s后再绑定关闭事件

显示成功,但是第三次点击按钮时,又变成以前那样了,同时触发显示和关闭,因为现在我有两个监听器都在监听了,我body上的监听应该移除

setTimeout(()=>{
            document.body.addEventListener('click',function x(){
              this.visible=false
              console.log('关闭了')
              document.body.removeEventListener('click',x)
              console.log('移除监听器了')
            }.bind(this))
          },1000)

发现还是一个样子,点击后的log倒是说明了一点问题

6ea4c6ca50df0f76935504b04c823ac7.png

这个监听器被重复的声明又移除,原因是事件的冒泡

我点击按钮,事件冒泡了三次,所以这个也触发了三次,我阻止了事件冒泡之后,发现功能实现了,但是log的次数反而增加了

我们将函数x放到外面去,同时发现了body的范围可能不是全屏的,我们换成document监听

showContent() {
      this.visible = !this.visible
      console.log('显示了')
      if (this.visible) {
        this.$nextTick(()=>{
          let eventHandler = () => {
            this.visible = false
            console.log('关闭了')
            document.removeEventListener('click', eventHandler)
            console.log('移除监听器了')
          }
          document.addEventListener('click',eventHandler)
        })
      }
    }

这样看似解决了所有问题,其实对于组件来说,这样的解决方法是不好的

几个问题:

1,假设我的popover外面的盒子上有overflowhidden,那就显示不出来了,所以这一套代码只是一个思路

2,不能用stop来解决冒泡的问题,会导致很多bug,会打断用户的事件链

最终的实现方式,将所有操作都抽离出来,一个函数不超过五行代码

     onClickDoc(e) {
      if (this.$refs.popover && (this.$refs.popover === e.target ||
          this.$refs.contentWrapper.contains( e.target ))) {
        return;
      }
      this.close()
    },
    open() {
      this.visible = true
      setTimeout( () => {
        this.positionContent()
        document.addEventListener( 'click', this.onClickDoc )
      } )
    },
    close() {
      this.visible = false
      document.removeEventListener( 'click', this.onClickDoc )
    },
    showContent(event) {
      if (this.$refs.triggerWrapper.contains( event.target )) {
        if (this.visible === true) {
          this.close()
        } else {
          this.open()
        }
      }
    }

实现position

和其他组件一样,接收一个props参数position,然后分别写样式,有些样式可以直接写css,但是有一些需要js操作

比如要控制弹出层的位置,需要拿到我们trigger按钮的宽高,需要拿到我们弹出层的宽高,而弹出层的宽高都是根据内容来的

我们使用一个hash表来存储每个方向上需要设置弹出层的位置

const {height:height2}=contentWrapper.getBoundingClientRect()
const {width,height,top, left} = triggerWrapper.getBoundingClientRect()
let positions={
        top:{
          top:top + window.scrollY,
          left:left + window.scrollX
        },
        bottom:{
          top:top + height+window.scrollY,
          left:left + window.scrollX
        },
        left:{
          top:top+window.scrollY+(height-height2)/2,
          left:left + window.scrollX
        },
        right:{
          top:top+window.scrollY+(height-height2)/2,
          left:left + window.scrollX+width
        }
      }

然后直接设置就好了

contentWrapper.style.left=positions[this.position].left+'px'
contentWrapper.style.top=positions[this.position].top+'px'

实现点击popover框中的按钮关闭

用户可以传一个按钮到popover中,这个按钮可以控制将popover关闭

使用插槽的slot-scoped

我们在插槽里面可以写一个div,这样div就能代替插槽的位置,但是我们又想让这个div能够使用组件里面的方法,就可以用slot-scoped来获取

第一步:将关闭的方法传出来

<slot name="content" +:close="close"></slot>

第二步:通过slot-scoped拿到这个close方法

<template slot="content" slot-scope="{close}">

第三步:组件中传的button中就可以使用close了

<button @click="close">关闭</button>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值