使用vue3+TypeScript手动封装tabs组件

简介: 使用vue3+TypeScript手动封装tabs组件

前言🚴‍♀🚴‍♀


之前在写项目的时候,用到了element-plus,在这个项目中需要使用tabs组件,于是我便萌生了一个想法:自己封装一个tabs组件,感觉应该也不是很难。


最终效果图


image.png


实现的思路和步骤🤔🤔


首先一个tabs我认为由两部分组成:头部和内容区域,头部区域也就是表头部分,那么这部分如何渲染呢?相信有些同学就会说,我们可以传递一个数组进去,但是这样下面显示的内容和表头就有些割裂的意思,所以当我查看element-plus官网时他是这么做的:

image.png

可见他是首先将el-tab-panel插入,然后在传入两项label和name,label代表这一项的标题,name可以理解为是区别不同项的标识

然后父组件通过useSlot获取到lable和name这个属性来进行渲染.


首先实现tabs这部分🛰🛰


整体上分为两部分,一部分是显示标题,一部分是要预留插槽为了将来插入tabs-panel准备

在样式布局方面我使用了bootstrap

tabs布局部分

<template>
  <div class="card text-center "
       :style="{maxWidth:650+'px'}">
    <div class="card-header">
      <ul class="nav  card-header-tabs">
        <li class="nav-item"
            v-for="titleInfo,index in titles"
            :key="index"     
            :class="{'active-style':titleInfo?.name===currentTab}"
            @click.prevent="selectTab(titleInfo?.name,index)">
          <a class="nav-link "
             href="#"
             ref="navLink">{{ titleInfo?.title }}</a>
        </li>
      </ul>
        <!--底部下划线部分,宽度会根据当前标题的宽度改变-->
      <div class="underline">
        <li :style="{left:updateLeft+'px',width:underlineWidth+'px'}"></li>
      </div>
    </div>
      <!--这部分是要插入的tabs-panel部分-->
    <div class="card-body">
      <slot></slot>
    </div>
  </div>
</template>


tabs中的ts代码

基本结构

这部分是基本的框架,下面我将具体拆分开来讲解:

<script lang="ts">
import 'bootstrap/dist/css/bootstrap.css'
import { onMounted, provide, ref, useSlots, watchEffect } from 'vue'
import { defineComponent } from 'vue'
import TabCustomPanel from './TabCustom-panel.vue'
export default defineComponent({
   //这个是tabs默认显示哪一个内容,默认值为空
  components: {
    TabCustomPanel,
  },
  name: 'Tabs',
  setup(props, { emit }) {
   
      //具体的代码......  
  })  
   return {
      titles,
      currentTab,
      selectTab,
      updateLeft,
      navLink,
      underlineWidth,
    }
  },
})
</script>

声明/接收变量🍆🍆

  1. 定义接受的变量的类型,并且设置默认值为空。
  2. 声明代码中需要的变量
export default defineComponent({
props: {
    default: {
      type: String,
      default: '',
      require: false,
    },
  }, 
setup(){
//通过useSlots方法获取插槽中的内容
    const slots = useSlots()
    //  获取underline的DOM元素
    let updateLeft = ref(0)
    // 获取nav-link的DOM元素(是"nav-item下的a元素")
    let navLink = ref(null)
    // navLink元素的宽度
    let navLinkWidth = ref(0)
    // 设置下滑线的宽度
    let underlineWidth = ref(0)
    // 定义一个currentTab,表示的时当前点击的标题,里面是标签的name属性,默认的值是default
    const currentTab = ref(props.default)
}
}) 

selectTab函数部分(实现切换功能)🍎🍎

 // 定义切换显示函数
    // 当切换标题的时候触发显示内容更新和下滑线的移动
    const selectTab = (name: string, titleIndex: number) => {
      //  当点击切换函数之后,重新将新的值赋给currentTab
      currentTab.value = name
      //  获取当前的a标签的宽度,依次来设置下划线的宽度
      navLinkWidth.value = (navLink.value as any)[titleIndex].clientWidth
      //  当切换标题的时候,更换下划线的宽度,下划线的宽度随标题的长度改变
      underlineWidth.value = navLinkWidth.value * 0.8
      //设置下划线的距离,利用了元素的offsetLeft属性,根据当前标题的offsetLeft来决定下划线的偏移量
      if (titleIndex === 0) {
        updateLeft.value = 0
      } else {
        updateLeft.value = (navLink.value as any)[titleIndex].offsetLeft
      }
      // 给自定义事件tab-click发送数据,子向父组件传值,在使用tabs的组件中可以获取当前标题的name属性
      emit('tabs-click', currentTab.value)
    }

这个函数功能比较全面:

  1. 首先实现了点击标题实现了标题的切换(currentTab表示的是当前点击的标题的name属性值)
  2. 实现了下滑线的宽度自适应:下划线的宽度会随着标题的宽度的改变而改变,并且改变下划线的位置
  3. 并且这个函数可以通过emit将值传递给父组件


使用onMounted在挂载组件时进行首次操作🎃🎃

  1. 在第一次渲染时,由于父组件传递的值default可能为空,因此要对其进行判断如果为空,则默认选中标题。
  2. 首次渲染时同样下划线的宽度也要跟选中的标题绑定,因此也要进行处理
 onMounted(() => {
  // 更新当前选中的tabBar,设置默认的标题栏
      const defaultTab = titles.find(
        (child) => child?.name === currentTab.value
      )
      // 获取当前所在标题栏的index
      let titleIndex = titles.findIndex(
        (item) => item?.name === currentTab.value
      )
       //进行这一步的判断的主要原因是因为default可能是空,由于currentTab的默认值就是default所以也可能为空
      //那么defaultTab就也为空,这个时候就要默认显示第一个标题的内容
      if (defaultTab) {
        // 设置currentTab的默认值
        currentTab.value = defaultTab.name
      } else if (titles.length > 0) {
        currentTab.value = titles[0]?.name
      }
      
      // 获取nav-link元素的宽度,并给underline设置宽度
      // 在刚挂载的时候,第一次下划线显示的位置
      // 判断当前选择的是哪一个标题,并且获取索引值
      const index = titles.findIndex((item) => item?.name === currentTab.value)
      //navLink是一个数组,里面是(是"nav-item下的a元素")
      navLinkWidth.value = (navLink.value as any)[index].clientWidth
      // 设置下划线的宽度
      underlineWidth.value = navLinkWidth.value * 0.8
      // 在组件挂载之后设置上一次的prevtitleIndex
    })

获取slot插槽中的元素

通过使用useSlot方法获取当前页面的默认插槽进而可以获取里面的props进而获取子组件上面的配置项"title"和"name"

// 获取slot插槽中的子组件上面的title和name属性,以便渲染navBar
    //解构出item中的props方法,最终返回titles数组里面
    const slots =useSlot();
    const titles = slots.default!().map(({ props }) => {
      if (props) {
        const { title, name } = props
        return {
          title,
          name,
        }
      }

使用provide方法将currentTab传递给子组件tab-panel,子组件由此通过v-if来判断显示那个内容

 //  将currentTab传递过去,注意要将其作为ref包裹的值传递过去否则会失去响应式
        //使用provide方法
    provide('currentTab', currentTab)

下划线样式以及选中标题时的样式

.active-style {
  box-shadow: -3px -3px 3px 3px rgba(209, 204, 204, 0.47);
  border-radius: 10%;
}
.underline {
  position: relative;
  li {
    transition: all 0.4s ease;
    height: 3px;
    background-color: rgb(126, 225, 225);
    position: absolute;
    a {
      box-sizing: border-box;
    }
  }
}


完成tab-panel部分 😙😙


<template>
  <!-- 判断插入的tab-panel是不是当前应该显示的 -->
  <div v-if="currentTab===name">
    <slot></slot>
  </div>
</template>

子组件tab-panel主要就是通过inject接收父组件传递过来的currentTab,来通过v-if来判断当前选中的标题。

<script lang="ts">
import { defineComponent, inject, ref, watch } from 'vue'
export default defineComponent({
  name: 'tabsCustomList',
  props: {
    title: {
      type: String,
      require: true,
    },
    name: {
      type: String,
      require: true,
    },
  },
  setup(props) {
     //接收tab传递过来的currentTab
    let currentTab = inject('currentTab') as string
    return {
      currentTab,
    }
  },
})
</script>


在页面中使用⛹️‍♀️⛹️‍♀️

  <div style="width: 700px;">
        <TabsCustom :default="'first'"
                    ref="tabsCustom"
                    @tabs-click="handleTabs">
          <TabsCustom-Panel title="省级"
                            name="first">
            <div ref="ecarts1"
                 id="demo"
                 style="height: 250px;"></div>
          </TabsCustom-Panel>
          <TabsCustom-Panel title="国际或者国家级"
                            name="second">
            <div ref="ecarts2"
                 style="height: 250px;"></div>
          </TabsCustom-Panel>
        </TabsCustom>
      </div>

当然我这里面配合了echarts使用,在自己使用的时候我们可以是显示文字或者其他图片等

当然,配合echarts使用会有一点小问题,是由于echart所导致的bug,这个我们留到下次来讲解


总结🗽🗽


完成了tabs组件的风封装,可以更加深刻的体会vue中的模块化组件的思想.并且自己也使用了许多新的技术,比如provide和inject,slot的使用等等.并且当完成这个组件之后我们会更加的有成就感.

相关文章
|
1月前
|
JavaScript 数据库
ant design vue日期组件怎么清空 取消默认当天日期
ant design vue日期组件怎么清空 取消默认当天日期
|
1天前
|
JavaScript 安全 开发者
Vue3 中对 TypeScript 的支持
Vue3 中对 TypeScript 的支持
|
2天前
|
JavaScript 前端开发
Vue组件生命周期深度剖析:从创建到销毁的八大钩子实战指南
Vue组件生命周期深度剖析:从创建到销毁的八大钩子实战指南
|
13天前
|
JavaScript 安全 前端开发
Vue 3 中的 TypeScript
【6月更文挑战第15天】
22 6
|
17天前
|
JavaScript
Vue.js中实现自定义组件的双向绑定
Vue.js中实现自定义组件的双向绑定
|
17天前
|
JavaScript
Vue.js中使用作用域插槽实现自定义表格组件
Vue.js中使用作用域插槽实现自定义表格组件
|
22天前
|
JavaScript 前端开发 API
vue的优缺点有那些 组件常用的有那些?
vue的优缺点有那些 组件常用的有那些?
|
2天前
|
JavaScript
Vue学习系列(二)——组件详解
Vue学习系列(二)——组件详解