使用vue3封装Popper组件

3,444 阅读1分钟

什么是Popper组件?

屏幕录制2022-03-27-上午5.19.gif

即使窗口变化、滚动,也不会让弹出框被遮挡

这是一个非常能让用户体验提升的功能

想实现这个功能其实并不困难

实现Popper组件

首先,给大家推荐一下 popper.js

popper.js 是个轻量级强大定位引擎利器

当然,他同时也被Element UI 作为 dependencies(打包依赖)使用

安装:

npm i @popperjs/core

使用:

import { createPopper } from '@popperjs/core'

createPopper(reference, popper, {
// options
})

reference 是挂载元素DOM

popper 是弹出元素DOM

接下来编写 Popper 组件

<template>
 <div
   class="reference"
   ref="referenceRef"
   @click.stop.prevent="togglePopperShow"
 >
   <slot></slot>
 </div>
 <div 
   ref="popperRef"
   v-show="visible"
 >
   <slot name="popper"></slot>
 </div>
</template><script lang="ts">
import { defineComponent, ref, unref, onMounted, watch, nextTick, onUnmounted } from 'vue'
import { createPopper } from '@popperjs/core'
export default defineComponent({
 name'Popper',
 props: {
   visible: {
     typeBoolean,
     default: false
  }
},
 emits: ['update:visible''hide''show'],
 setup (props, { emit }) {
   const referenceRef = ref<HTMLElement>(null)
   const popperRef = ref<HTMLElement>(null)
   let popperInstance = null
   // 创建 popper 实例
   const createPopperInstance = () => {
     popperInstance = createPopper(unref(referenceRef), unref(popperRef), {
       modifiers: [
        {
           name'offset',
           options: {
             // 偏移值 左右,上下
             offset: [05]
          }
        }
      ]
    })
     nextTick(() => {
       // 异步更新
       popperInstance.update()
    })
  }
​
   // 销毁 popper 实例
   const destroyPopperInstance = () => {
     popperInstance?.destroy?.()
     popperInstance = null
  }
​
   watch(() => props.visible,(visible) => {
     if (visible) {
       createPopperInstance()
       emit('show')
    } else {
       emit('hide')
    }
  })
​
   onMounted(() => {
     createPopperInstance()
  })
​
   onUnmounted(() => {
     destroyPopperInstance()
  })
​
   const togglePopperShow = () => {
     emit('update:visible', !props.visible)
  }
​
   return {
     referenceRef,
     popperRef,
     togglePopperShow
   }
  }
})
</script>

引用 Popper 组件

<template>
 <popper v-model:visible="show">
   <div class="box"></div>
   <template #popper>
     <div class="popper"></div>
   </template>
 </popper>
</template><script lang="ts">
import { defineComponent, ref } from 'vue'
import Popper from './Popper.vue'
export default defineComponent({
 components: {
   Popper
},
 setup() {
   const show = ref(false)
   return {
     show
  }
}
})
</script><style scoped lang="less">
.box {
 width100px;
 height100px;
 background: pink;
}
​
.popper {
 width100px;
 height50px;
 background: lightskyblue;
}
</style>

屏幕录制2022-03-27-上午4.03.gif

好了,效果就出来了

但是有些僵硬,那就加些动画

<!-- Popper 组件 -->
<!-- 省略部分代码 -->
<transition>
 <div
      ref="popperRef"
      v-show="visible"
      >
   <slot name="popper"></slot>
 </div>
</transition>
<!-- 省略部分代码 -->
​
​
<style scoped lang="less">
.reference {
 display: inline-block;
}
​
.v-enter-active,
.v-leave-active {
 transition: opacity .3s ease-out, transform .3s ease-out;
 opacity:1;
 transformscaleY(1);
 transform-origin: center top;
}
​
[data-popper-placement='top'] {
 transform-origin: center bottom;
}
​
.v-enter-from,
.v-leave-to {
 opacity0;
 transformscaleY(0);
}
​
</style>

发现动画并不生效,同时有警告

Popper: Detected CSS transitions on at least one of the following CSS properties: "transform", "top", "right", "bottom", "left". 
​
Disable the "computeStyles" modifier's `adaptive` option to allow for smooth transitions, or remove these properties from the CSS transition declaration on the popper element if only transitioning opacity or background-color for example. 

说的是:我们在Popper元素上设置了 transform 样式

那我们就按照他的提示,设置一下Popper参数

// 省略部分代码
// 创建 popper 实例
const createPopperInstance = () => {
 popperInstance = createPopper(unref(referenceRef), unref(popperRef), {
   modifiers: [
    {
       name'offset',
       options: {
         offset: [05]
      }
    },
    {
       // 禁用计算样式,使用 transition 动画
       name'computeStyles',
       options: {
         gpuAccelerationfalse,
         adaptivefalse
      }
    }
  ]
 })
 nextTick(() => {
   // 异步更新
   popperInstance.update()
 })
}

完成,很丝滑~

屏幕录制2022-03-27-上午4.17.gif

继续测试一下

屏幕录制2022-03-27-上午5.11.gif

好,完美~

完整代码

<template>
  <div
    class="reference"
    ref="referenceRef"
    @click.stop.prevent="togglePopperShow"
  >
    <slot></slot>
  </div>
  <transition>
    <div
      ref="popperRef"
      v-show="visible"
    >
      <slot name="popper"></slot>
    </div>
  </transition>
</template>

<script lang="ts">
import { defineComponent, ref, unref, onMounted, watch, nextTick, onUnmounted } from 'vue'
import { createPopper } from '@popperjs/core'
export default defineComponent({
  name: 'Popper',
  props: {
    visible: {
      type: Boolean,
      default: false
    }
  },
  emits: ['update:visible', 'hide', 'show'],
  setup (props, { emit }) {
    const referenceRef = ref<HTMLElement>(null)
    const popperRef = ref<HTMLElement>(null)
    let popperInstance = null
    // 创建 popper 实例
    const createPopperInstance = () => {
      popperInstance = createPopper(unref(referenceRef), unref(popperRef), {
        modifiers: [
          {
            // 偏移值 左右,上下
            name: 'offset',
            options: {
              offset: [0, 5]
            }
          },
          {
            // 禁用计算样式,使用 transition 动画
            name: 'computeStyles',
            options: {
              gpuAcceleration: false,
              adaptive: false
            }
          }
        ]
      })
      nextTick(() => {
        // 异步更新
        popperInstance.update()
      })
    }

    // 销毁 popper 实例
    const destroyPopperInstance = () => {
      popperInstance?.destroy?.()
      popperInstance = null
    }

    // 监听 visible 属性
    watch(() => props.visible,(visible) => {
      if (visible) {
        createPopperInstance()
        emit('show')
      } else {
        emit('hide')
      }
    })

    onMounted(() => {
      createPopperInstance()
    })

    onUnmounted(() => {
      destroyPopperInstance()
    })

    const togglePopperShow = () => {
      emit('update:visible', !props.visible)
    }

    return {
      referenceRef,
      popperRef,
      togglePopperShow
    }
  }
})
</script>

<style scoped lang="less">
.reference {
  display: inline-block;
}

.v-enter-active,
.v-leave-active {
  transition: opacity .3s ease-out, transform .3s ease-out;
  opacity:1;
  transform: scaleY(1);
  transform-origin: center top;
}

[data-popper-placement='top'] {
  transform-origin: center bottom;
}

.v-enter-from,
.v-leave-to {
  opacity: 0;
  transform: scaleY(0);
}

</style>

感谢阅读