Vue:造轮子-05:tabs组件

1,870 阅读2分钟

Vue:造轮子-05:tabs组件

需求

  • 点击 Tab 切换内容
  • 有一条横线在动
  1. 设计API
<Tabs>
  <Tab title="导航1">内容1</Tab>
  <Tab title="导航2"><Component1/></Tab>
  <Tab title="导航3"><Component1 x="hi"/></Tab>
</Tabs>
  1. 新建Tab和Tabs组件,TabsDemo.vue

如何在运行时确认子组件的类型

  1. 问题:
  • tabsDemo.vue中
<template>
    <h1>tabs </h1>
    <h2>示例1</h2>
    <Tabs>
        <Tab title="导航1">内容1</Tab>
        <Tab title="导航2">内容2</Tab>
    </Tabs>
</template>

在上面的代码中,如果用户在里写的不是,而是其他标签,如

应该怎么检测? 2. 使用context.slots.default() 数组

  • context.slots.default() 数组,代表中传进来的两个
  • Tabs.vue中
setup(props,context){
            //console.log({...context.slots.default()[0] })
            //console.log({...context.slots.default()[1] })
            const defaults = context.slots.default()
          //  console.log(defaults[0].type === tab) //检查组件类型
            defaults.forEach((tag)=>{
                if(tag.type !== tab){ //如果自组件的类型不是tab,则报错
                    throw new Error('tabs必须是tab') //如果这里报错,下面不会执行
                }
            })
            const titles = defaults.map((tag)=>{
                    return tag.props.title //拿到tab的title属性
            })
            return {defaults,titles}
        }

如何渲染嵌套的组件 - 嵌套插槽

  1. 问题:
  • tabsDemo.vue中
<template>
    <h1>tabs </h1>
    <h2>示例1</h2>
    <Tabs>
        <Tab title="导航1">内容1</Tab>
        <Tab title="导航2">内容2</Tab>
    </Tabs>
</template>

组件里有一个插槽,渲染组件。组件里还需要一个插槽渲染里的东西。 2.

  • Tabs.vue用component
<template>
    <div v-for="(t,index) in titles" :key="index">{{t}}</div>
    <component v-for="(c,index) in defaults" :is="c" :key="index"></component>
</template>
  • Tab.vue:用slot
<template>
<div>
    <slot></slot>
</div>
</template>

  1. 获取title和content
  • 使用context.slots.default()
  • tabs.vue
<template>
    <div v-for="(t,index) in titles" :key="index">{{t}}</div>
    <component v-for="(c,index) in defaults" :is="c" :key="index"></component>
</template>

<script lang="ts">
    import tab from './tab.vue'
    export default {
        name: "tabs",

        setup(props,context){
            //console.log({...context.slots.default()[0] })
            //console.log({...context.slots.default()[1] })
            const defaults = context.slots.default()
          //  console.log(defaults[0].type === tab) //检查组件类型
            defaults.forEach((tag)=>{
                if(tag.type !== tab){ //如果自组件的类型不是tab,则报错
                    throw new Error('tabs必须是tab') //如果这里报错,下面不会执行
                }
            })
            const titles = defaults.map((tag)=>{
                    return tag.props.title //拿到tab的title属性
            })
            return {defaults,titles}
        }

    }
</script>

切换标签页

  1. 如何表示选中那个tab呢?
  • 这里用title表示
<Tabs selected="导航1">
  <Tab title="导航1">内容1</Tab>
  <Tab title="导航2"><Component1/></Tab>
  <Tab title="导航3"><Component1 x="hi"/></Tab>
</Tabs>
  1. 选中导航
  • 哪个导航被选中了,就在上面加一个class
  • 当title和props接收的title相等时,增加selected属性
  • Tabs.vue中
<div class="gulu-tabs-nav-item"
     :class="{selected: t === selected}" @click="select(t)"
     v-for="(t,index) in titles" :key="index">{{t}}
</div>


// script
  props:{
            selected:{
                type:String
            }
        },
  1. 选中内容
  • 第一次错误
  • 第二次错误
  • 最终用css实现
    用class控制是否展示 tabs.vue
<div class="gulu-tabs-content">
            <component class="gulu-tabs-content-item" 
            :class="{selected: c.props.title === selected }" 
            v-for="c in defaults" :is="c" />
</div>

css: 正常是display:none,选中的话加class:selected,变为display:block

  &-content {
            padding: 8px 0;
            &-item{
                display:none;
                &.selected {
                    display: block;
                }
            }
        }

制作会动的横线

title下应该有个横线,一个 div 即可搞定

  1. 在title下增加一个indicator的div tabs.vue
<div class="gulu-tabs-nav">
       <div class="gulu-tabs-nav-item" v-for="(t,index) in titles" @click="select(t)" :class="{selected: t=== selected}" :key="index">{{t}}
       </div>
        <div class="gulu-tabs-nav-indicator">

        </div>
</div>

css

&-indicator {
                position: absolute;
                height: 3px;
                background: $blue;
                left: 0;
                bottom: -1px;
                width: 100px;
            }
  1. 如何确定横线的长度呢? 长度应该和title的长度差不多
  • 先去找title的长度,发现时for出来的。
  • 使用template refs
//template
<div class="gulu-tabs-nav">
            <div class="gulu-tabs-nav-item"
                 v-for="(t,index) in titles"
                 :ref="el=>{if(el) navItems[index]=el}"
                 @click="select(t)"
                 :class="{selected: t=== selected}" :key="index">{{t}}
            </div>
            <div class="gulu-tabs-nav-indicator"
                 ref="indicator" >
            </div>
</div>


//setup里面: 
  const navItems = ref<HTMLDivElement[]>([])
  const indicator = ref<HTMLDivElement>(null)
  onMounted(()=>{ // 挂载后显示
                console.log({...navItems.value})
                const divs = navItems.value
                const result = divs.filter(div=>div.classList.contains('selected'))[0] //filter返回数组,所以获取[0]。获取到了被选中的title的div
                console.log(result)
                const {width} = result.getBoundingClientRect() //获取宽度
                indicator.value.style.width = width + 'px'
            })
  1. 让indicator动起来(跟随当前被选中的导航)
  • 确定移动的距离 gLWy4O.png
  const {left:left1} = container.value.getBoundingClientRect() //获取container的left                
  const {left:left2} = result.getBoundingClientRect() //result 的left
  const left = left2 - left1
  indicator.value.style.left = left + 'px'