模仿el-popover实现自己的vue组件

效果

样式效果

未点击时候是这样的
在这里插入图片描述
点击按钮,弹出弹框,弹框是主动添加到body下的标签
在这里插入图片描述
拉动浏览器窗口,弹框可以自动调节显示位置
在这里插入图片描述

代码效果:

代码调用和 el-popover 保持一致。

  1. 使用具名插槽reference,来显示触发按钮
  2. 使用默认插槽来显示弹框
<template>
  <div class="clips">
    <lm-popover popper-class="customClass"  trigger="click">
      <div class="content">
        我是要显示的内容
      </div>
      <template v-slot:reference>
        <el-button>成功按钮</el-button>
      </template>
    </lm-popover>
  </div>
</template>
<script>
import  lmPopover from './lm-popover.vue'
export default {
  components: {
    lmPopover
  }
}
</script>
<style>
.customClass.content{
  	
}
</style>

源码与解析

解析
  1. 如何创建组件,并且绑定到body中

看一段入口文件main.js的代码:
在这里插入图片描述
是不是特别的熟悉呢? 实例化vue,render的实际上是渲染App.vue组件。并绑定到#app上,那么做一个改造吧,将其绑定到自己生成的div中:

this.dom = new Vue({
        el: document.createElement('div'),
        render: (createElement) => { // createElement是render的回调参数,作用是创建虚拟dom,
        // return 创建的虚拟dom
        },
      })
document.body.appendChild(this.dom.$el);

el相当于上图的 m o u n t , 加 真 实 D O M 绑 定 到 手 动 创 建 的 d i v 中 , 在 调 用 组 件 中 申 明 一 个 变 量 d o m 等 于 实 例 化 的 v u e , 就 能 得 到 这 个 v u e 的 实 例 化 对 象 了 。 就 是 能 用 到 t h i s 中 的 数 值 了 。 再 通 过 将 mount,加真实DOM绑定到手动创建的div中, 在调用组件中申明一个变量dom等于实例化的vue,就能得到这个vue的实例化对象了。就是能用到this中的数值了。 再通过将 mountDOMdivdomvuevuethisel将标签绑定到body上。就能渲染页面了。
createElement 如何使用呢?如下,也可以看下vue官网介绍 createElement 使用规则

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一个 HTML 标签名、组件选项对象,或者
  // resolve 了上述任何一种的一个 async 函数。必填项。
  'div',

  // {Object}
  // 一个与模板中 attribute 对应的数据对象。可选。
  {
    // (详情见下一节)
  },

  // {String | Array}
  // 子级虚拟节点 (VNodes),由 `createElement()` 构建而成,
  // 也可以使用字符串来生成“文本虚拟节点”。可选。
  [
    '先写一些文字',
    createElement('h1', '一则头条'),
    createElement(MyComponent, {
      props: {
        someProp: 'foobar'
      }
    })
  ]
)
{
  // 与 `v-bind:class` 的 API 相同,
  // 接受一个字符串、对象或字符串和对象组成的数组
  'class': {
    foo: true,
    bar: false
  },
  // 与 `v-bind:style` 的 API 相同,
  // 接受一个字符串、对象,或对象组成的数组
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 普通的 HTML attribute
  attrs: {
    id: 'foo'
  },
  // 组件 prop
  props: {
    myProp: 'bar'
  },
  // DOM property
  domProps: {
    innerHTML: 'baz'
  },
  // 事件监听器在 `on` 内,
  // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。
  // 需要在处理函数中手动检查 keyCode。
  on: {
    click: this.clickHandler
  },
  // 仅用于组件,用于监听原生事件,而不是组件内部使用
  // `vm.$emit` 触发的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定义指令。注意,你无法对 `binding` 中的 `oldValue`
  // 赋值,因为 Vue 已经自动为你进行了同步。
  directives: [
    {
      name: 'my-custom-directive',
      value: '2',
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 作用域插槽的格式为
  // { name: props => VNode | Array<VNode> }
  scopedSlots: {
    default: props => createElement('span', props.text)
  },
  // 如果组件是其它组件的子组件,需为插槽指定名称
  slot: 'name-of-slot',
  // 其它特殊顶层 property
  key: 'myKey',
  ref: 'myRef',
  // 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
  // 那么 `$refs.myRef` 会变成一个数组。
  refInFor: true
}

因此我们贴上我的源码来分析:
第一个参数 :popover 使我们手动写的popover.vue文件组件,相当于一个壳子, 导入后放在这里。 createElement实际上就是依靠这个popover壳子组件来执行,给出props等传参的值,最终生成虚拟DOM。this.$slots.default是当前将外部调用的默认插槽放到popover中的子元素插槽。

new Vue({
        el: document.createElement('div'),
        render: (createElement) => {
          return createElement(popover, {
            // 绑定参数
          }, this.$slots.default)
        },
      })
源码
  • lm-popover.vue
<template>
  <div
    class="insertDom"
    @click="clickTrigger"
  >
    <slot name="reference"></slot>
  </div>
</template>

<script>
import Vue from 'vue'
import popover from './popover.vue'
export default {
  data () {
    return {
      dom: null
    }
  },
  props: {
    // 自定义内容样式
    popperClass: {
      type: String
    },
    // 内容部分是否需要按钮
    closeBtn: {
      type: Boolean,
      default: false
    }
  },

  methods: {
    // 创建弹框
    create (e) {
      this.dom = new Vue({
        el: document.createElement('div'),
        render: (createElement) => {
          return createElement(popover, {
            props: {
              popperClass: this.$props.popperClass,
              closeBtn: this.$props.closeBtn,
              hide: this.hide,
              createDom: this.$el
            },
            style: {
              left: -1000 + 'px',
              top: -1000 + 'px',
            }
          }, this.$slots.default)
        },
      })
      document.body.appendChild(this.dom.$el);
    },
    // 删除弹框
    remove () {
      if (this.dom !== null) {
        document.body.removeChild(this.dom.$el);
      }
    },
    // 显示弹框
    show () {
      this.dom.$el.style.display = 'block'
    },
    // 隐藏弹框
    hide () {
      this.dom.$el.style.display = 'none'
      this.$emit('hide')
    },
    // 触发事件
    async clickTrigger (e) {

      if (this.dom === null) {
        await this.create(e)
      } else {
        this.show()
      }

      this.$emit('show')
    }
  },
  // 组件销毁,自动再body中删除对应内容
  beforeDestroy () {
    this.remove()
  },
}
</script>

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

  • popover.vue
<template>
  <div
    class="popover"
    :class="$props.popperClass"
  >
    <div
      class="close"
      @click="close"
      v-if="closeBtn"
    ></div>
    <slot></slot>
  </div>
</template>

<script>
export default {
  props: {
    popperClass: {
      type: String
    },
    closeBtn: {
      type: Boolean
    },
    hide: {
      type: Function
    },
    createDom: {
    }

  },

  methods: {
    setPosition () {
      // 触发事件的元素
      let posEl = this.$props.createDom.getBoundingClientRect()
      let posSelf = this.$el.getBoundingClientRect()

      // 滚动高度,可视区域高度
      var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
      var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
      let windowWidth = document.documentElement.clientWidth || document.body.clientWidth;
      let windowHeight = document.documentElement.clientHeight || document.body.clientHeight;

      let posX = 0
      let posY = 0

      if (windowWidth + scrollLeft - (posEl.width + posEl.left + posSelf.width + 8) > 0) {
        // 属于正常部分
        posX = posEl.width + posEl.left + 8
      } else {
        // 不正常部分
        let offsetWidth = windowWidth + scrollLeft - (posEl.width + posEl.left + posSelf.width + 8)
        posX = posEl.width + posEl.left + 8 + offsetWidth
      }
      if (windowHeight + scrollTop - (posEl.height + posEl.top + posSelf.height + 8) > 0) {
        // 属于正常部分
        posY = posEl.height + posEl.top + 8
      } else {
        // 不正常部分
        let offsetHeight = windowHeight + scrollTop - (posEl.height + posEl.top + posSelf.height + 8)
        posY = posEl.height + posEl.top + 8 + offsetHeight

      }
      // 设置位置
      this.$el.style.left = posX + 'px'
      this.$el.style.top = posY + 'px'
    },
    close () {
      this.$props.hide()
    }
  },
  mounted () {

    // 设置当前的位置
    setTimeout(() => {
      this.setPosition()
    }, 100)


    this.otherHide = (e) => {
      if (!this.$el.contains(e.target) && !this.$props.createDom.contains(e.target)) {
        this.$props.hide()
      }
    }
    window.addEventListener('click', this.otherHide)
    window.addEventListener('resize', this.setPosition)
  },
  beforeDestroy () {
    window.removeEventListener('click', this.otherHide)
  },
}
</script>

<style scoped lang="less">
.popover {
  position: absolute;
  padding: 8px;
  background-color: #fff;
  z-index: 2047;
  color: #606266;
  box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
  min-width: 150px;
  border-radius: 4px;
  border: 1px solid #ebeef5;
  word-break: break-all;
  .close {
    position: absolute;
    right: 0;
    top: 0;
    background-color: #ccc;
    width: 20px;
    height: 20px;
  }
}
</style>

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值