Vue:造轮子-05:tabs组件
需求
- 点击 Tab 切换内容
- 有一条横线在动
- 设计API
<Tabs>
<Tab title="导航1">内容1</Tab>
<Tab title="导航2"><Component1/></Tab>
<Tab title="导航3"><Component1 x="hi"/></Tab>
</Tabs>
- 新建Tab和Tabs组件,TabsDemo.vue
如何在运行时确认子组件的类型
- 问题:
- 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}
}
如何渲染嵌套的组件 - 嵌套插槽
- 问题:
- 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>
- 获取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>
切换标签页
- 如何表示选中那个tab呢?
- 这里用title表示
<Tabs selected="导航1">
<Tab title="导航1">内容1</Tab>
<Tab title="导航2"><Component1/></Tab>
<Tab title="导航3"><Component1 x="hi"/></Tab>
</Tabs>
- 选中导航
- 哪个导航被选中了,就在上面加一个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
}
},
- 选中内容
- 第一次错误
- 第二次错误
- 最终用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 即可搞定
- 在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;
}
- 如何确定横线的长度呢? 长度应该和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'
})
- 让indicator动起来(跟随当前被选中的导航)
- 确定移动的距离
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'