使用render方法造一个简单的vue Tooltip

1,708 阅读4分钟

前言

端午节大家粽子可吃的开心?你问我为啥又要自己造轮子?那当然是闲的蛋疼了。因为不想引入一个大而全的UI库,虽然有些也提供了插件进行按需加载,不过试了下Element-ui和iView,一个和最新版Nuxt在ts模式下有不兼容问题,另一个则影响了全局样式,况且自己的场景很简单,不需要什么复杂的功能,那还是自己撸一个更快点。

定需求

  1. 只要hover时展示一句提示
  2. 提供简单几种不同方向的定位

开搞

因为用的.vue后缀格式文件,所以实际上都放在一起的,为了方便说明,分开清晰一些。

第一版

因为很简单,直接上代码吧

模板:

<template>
  <div
    :class="{ 'm-tooltip': !disabled }"
    :data-content="content"
  >
    <slot />
  </div>
</template>

<script>
export default {
  props: {
    content: String, // 提示内容
    disabled: Boolean, // 是否关闭提示
  },
}
</script>

样式:

<style lang="scss">
.m-tooltip {
  position: relative;
  $y-space: 2px;
  $bg-color: rgba(70, 76, 91, 0.9);
  $shadow: 0 1px 6px rgba(0, 0, 0, 0.2);

  @mixin position() {
    position: absolute;
    left: 50%;
    bottom: 100%;
    transform: translateX(-50%);
    opacity: 0;
    pointer-events: none;
  }

  &::before {
    content: attr(content); // 使用自定义属性的内容作为文本,这里直接使用了不标准的content属性
    @include position();
    line-height: 1.5;
    font-size: 12px;
    max-width: 250px;
    margin-bottom: 8px + $y-space;
    padding: 8px 12px;
    color: #fff;
    text-align: left;
    text-decoration: none;
    background-color: $bg-color;
    border-radius: 4px;
    box-shadow: $shadow;
    white-space: nowrap;
  }

  &::after {
    content: '';
    @include position();
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-bottom: $y-space;
    border-color: transparent;
    border-style: solid;
    border-width: 5px 5px 0;
    border-top-color: $bg-color;
  }

  &[right] {
    &::before {
      transform: translateX(-70%);
    }
  }

  &[left] {
    &::before {
      transform: translateX(-30%);
    }
  }

  &[show],
  &:hover {
    &::before,
    &::after {
      opacity: 100;
      pointer-events: unset;
      transition: all 0.2s ease-in-out;
    }
  }
}
</style>

使用方式:

  <Tooltip
    :disabled="!isLiked"
    right
    content="您已喜欢过该文章啦~"
  >
    <div
      :class="{ 'c-red': true, active: isLiked }"
      @click="like(detail.id)"
    >
      <i class="iconfont icon-xin" />
    </div>
  </Tooltip>

是不是很简单?只有两个属性,一个提示内容,另一个是否禁用。还有一个样式上的api,如果传了show属性,则会一直显示,用于手动控制显隐的情况。

样式上参考了iView的,因为懒得自己去画了,还是相信人家设计师的功底的:)。暂时只用到上面的方向,所以通过传left\right属性来控制是否偏左或偏右。

显示效果:

demo1.png

咦,好像哪里不对劲?想了想,原来是样式有问题,hover时内部子元素应该有边框高亮才对。(我就知道事情没那么简单)

dom1

这样实现会造成中间加了一层元素,可能会影响原有子类选择器的样式,那怎么把包裹的这一层当做空气一样透明的去掉呢?

第二版

翻了下Vue官网文档,真不错哟,看到了jsx。好家伙,这就是本大爷的地盘了啊,自己控制渲染,嘿嘿嘿,那不是想怎么来就怎么来?

Tooltip进化~~

// 注意这里要把template删掉,不然还是会用模板渲染

<script>
export default {
  props: {
    disabled: Boolean, // 把其他几个透传的属性去掉,直接用就好了
  },
  render(createElement) {
    const vnodes = this.$slots.default
    if (vnodes.length === 1) {
      const node = vnodes[0]
      if (node.tag === 'div' || node.tag === 'span') {
        node.data.class = node.data.class || {}
        // 根据disabled控制是否添加样式类
        node.data.class['m-tooltip'] = !this.disabled
        // 其他属性直接透传到子元素上
        node.data.attrs = Object.assign(node.data.attrs || {}, this.$attrs)
      }
    } else {
      console.warn('Tooltip只接受一个子节点')
    }
    return this.$slots.default[0] // 直接返回第一个子节点
  },
}
</script>

中间发现不知道传的自定义属性是在什么位置,比如传了content是在放在子节点哪里呢?于是在子节点上写了一个自定义属性,打印后发现是在node.data.attrs对象里。

效果图:

demo2.png

dom结构:

dom2

嗯,这才对嘛,这样就把包裹元素上的属性都移到了子节点上,当然会产生属性覆盖的问题,不过自己用的话,倒是不太担心会有这个问题。至于其他子节点是否支持其他自定义组件,emmm,那就由各位自由发挥了。

样式和使用方式没变化,至于为啥使用没有data-前缀的非标准属性,那当然是因为啦,程序猿天生想少打几个字。

好了,自己写一个虽然比较简单,不过也够用了。

后记

嘛,想想现在,想做点自己的小东西吧,总是要用到一堆的框架,还没写代码呢,就开始各种痛苦的配置(你知道我在说什么)和各种库的踩坑。但你说不用吧,那只能翻一翻小的库,至于有多少坑,好不好用,那自然用了才知道啊。

反正就是爱折腾,不仅折腾别人,也折腾自己,哈哈哈。

ps: 这里不支持vue和scss格式的高亮嘛?