Tabs组件使用方式
<template>
<h1>示例1</h1>
<Tabs>
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
</template>
如何判断子组件的类型?
将context.slots打印出来,context.slots.default()得到的就是子组件数组
<template>
<div>Tabs组件</div>
<component :is="defaults[0]"/>
<component :is="defaults[1]"/>
</template>
<script lang="ts">
import Tab from './Tab.vue'
export default {
setup(props,context){
const defaults = context.slots.default()
defaults.forEach(tag => {
if(tag.type !== Tab) {
throw new Error('Tabs的子标签必须为Tab')
}
})
return {
defaults
}
}
}
</script>
渲染嵌套的插槽
props属性中可以获取到tab组件的props
<template>
<div>Tabs组件</div>
<div v-for="title in titles" :key="title">{{title}}</div>
<component v-for="(tab,index) of defaults" :is="tab" :key="index"/>
</template>
<script lang="ts">
import Tab from './Tab.vue'
export default {
setup(props,context){
const defaults = context.slots.default()
defaults.forEach(tag => {
if(tag.type !== Tab) {
throw new Error('Tabs的子标签必须为Tab')
}
})
const titles = defaults.map(tab => tab.props.title)
return {
defaults,
titles
}
}
}
</script>
实现tab切换
<template>
<h1>示例1</h1>
<Tabs v-model:selected="title">
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
</template>
<script lang="ts">
import { ref } from 'vue'
import Tab from '../lib/Tab.vue'
import Tabs from '../lib/Tabs.vue'
export default {
components: {
Tab, Tabs
},
setup(){
const title = ref('导航1')
return {
title
}
}
}
</script>
component需要绑定key才能感知到数据更新重新渲染
<template>
<div class="pika-tabs">
<div class="pika-tabs-nav" >
<div class="pika-tabs-nav-item" :class="{selected: title === selected}" v-for="title in titles" :key="title" @click="changeTab(title)">{{title}}</div>
</div>
<div class="pika-tabs-content">
<component class="pika-tabs-content-item" :is="current" :key="selected"/>
</div>
</div>
</template>
<script lang="ts">
import { computed } from 'vue'
import Tab from './Tab.vue'
export default {
props:{
selected: String
},
setup(props,context){
const defaults = context.slots.default()
defaults.forEach(tag => {
if(tag.type !== Tab) {
throw new Error('Tabs的子标签必须为Tab')
}
})
const changeTab = (value) => {
context.emit('update:selected', value)
}
const titles = defaults.map(tab => tab.props.title)
const current = computed(() => defaults.find(item => item.props.title === props.selected))
return {
defaults,
titles,
changeTab,
current
}
}
}
</script>
title下面添加动态指示条
在title下面添加指示条
添加一个div, 绝对定位
<div class="pika-tabs-nav" >
// ...
<div class="pika-tabs-nav-indicator"></div>
</div>
获取宽度
要获取到title的宽度,需要先用ref获取到title所在的DOM
参考文档
<template>
<div class="pika-tabs">
<div class="pika-tabs-nav" >
<div class="pika-tabs-nav-item"
:ref="el => { if (el) divs[index] = el }"
:class="{selected: title === selected}"
v-for="(title,index) in titles"
:key="index"
@click="changeTab(title)">{{title}}</div>
<div class="pika-tabs-nav-indicator"></div>
</div>
// ...
</div>
</template>
<script lang="ts">
import { computed,onMounted,ref } from 'vue'
// ...
export default {
// ...
setup(props,context){
const divs = ref([])
onMounted(()=>{
console.log(...divs.value)
})
// ...
return {
// ...
divs
}
}
}
</script>
获取DOM节点的宽度, 使用Node.getBoundingClientRect()
方法
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置
<template>
<div class="pika-tabs">
// ...
<div ref="indicator" class="pika-tabs-nav-indicator"></div>
</div>
// ...
</div>
</template>
<script lang="ts">
import { computed,onMounted,onUpdated, ref } from 'vue'
export default {
props:{
selected: String
},
setup(props,context){
const defaults = context.slots.default()
const navItems = ref([])
const indicator = ref(null)
onMounted(()=>{
const divs = navItems.value
const result = divs.filter(item => item.classList.contains('selected'))[0]
const {width} = result.getBoundingClientRect()
indicator.value.style.width = width + 'px'
})
onUpdated(()=> {
// 同上
})
// ...
}
}
</script>
计算left
setup(props,context){
const defaults = context.slots.default()
const navItems = ref([])
const indicator = ref(null)
const container = ref(null)
const x = () => {
const divs = navItems.value
const result = divs.filter(item => item.classList.contains('selected'))[0]
const {width, left: left2} = result.getBoundingClientRect()
indicator.value.style.width = width + 'px'
const {left: left1} = container.value.getBoundingClientRect()
indicator.value.style.left = (left2 - left1) + 'px'
}
onMounted(x)
onUpdated(x)
}
优化
onMounted和onUpdated可以使用watchEffect代替,注意watchEffect会在mounted前执行,导致获取不到DOM节点,所以需要放在onMounted里
onMounted(()=>{
watchEffect(() => {
console.log(selectedItem.value)
const {width, left: left2} = selectedItem.value.getBoundingClientRect()
indicator.value.style.width = width + 'px'
const {left: left1} = container.value.getBoundingClientRect()
indicator.value.style.left = (left2 - left1) + 'px'
})
})