Tabs 组件
标签页组件
需求
-
点击 Tab 切换内容
-
有一条横线在动
API 设计
- Tabs 组件怎么用
<Tabs>
<Tab title="导航1">内容1</Tab>
<Tab title="导航2"><Component1 /></Tab>
<Tab title="导航3"><Component1 x="hi" /></Tab>
</Tabs>
或
<Tabs :data="[
{title:'导航1',content: '内容1'},
{title:'导航2',content: Component1},
{title:''导航3,content: h(Compomnent1,{x:'hi'})},
]"/>
第二种写法就要兼容更多的方案,如果想随时新增一个type,就可以直接在data里面新增一个数组项就可以了,第一种方案不能直接加,只能用<Tab v-for="item in items">,修改items
创建Tabs和Tab组件
在lib里面创建Tabs.vue和Tab.vue
如何检查子组件的类型
如何在运行时确认子组件的类型 :检查context.slots.default()数组
用JS获取插槽内容:const defaults = context.slots.default()
如果能够检查Tab的类型,就来到Tabs.vue检查
首先在Tabs.vue里面使用console.log调试法,
<script lang="ts">
export default {
setup(props, context) {
console.log({
...context.slots.default()[0],
});
console.log({
...context.slots.default()[1],
});
},
};
</script>
打印出...context,找到slots,然后打印出...context.slots,会出现default,然后再打印出...context.slots.default(),在这里default是个函数,后面需要加括号。
可以通过拿到context.slots.default()的结果获取外面传给我们子内容,然后再看每个内容的标签。
通过检查defaults的第n个type来判断子组件的类型,若为true,则子组件的类型正确,否则为false。
Vue深入的原理:每一个Tab.vue,最终会导出成一个对象(Tab),因为这个对象和这个type是全等的,type里面的属性有一个render属性,那么我们写的.vue文件最终都会被变成一个Tab,那么Tab就是这么一个对象(如图)
<template>
<div>
Tabs 组件
<component :is="defaults[0]" />
<component :is="defaults[1]" />
</div>
</template>
<script lang="ts">
import Tab from "./Tab.vue";
export default {
setup(props, context) {
const defaults = context.slots.default();
//default是个函数,必须加()
console.log(defaults[0].type === Tab);
//检查子组件的类型
return { defaults };
},
};
</script>
防御性编程:如果代码出错,就不再执行,会报出错误。
渲染嵌套的插槽
不能直接插入插槽<slot /> ,可以用component实现插槽
<component :is="defaults[0]" />
<component :is="defaults[1]" />
//等价于
<component v-for="c in defaults" :is="c" />
// compomnent 实现插槽
通过每个defaults遍历tag得到tag.props.title的值,返回titles,添加一个div标签,将TabsDemo.vue的title内容插到div里面。
注意:用v-for时,必须要写:key
显示被选中内容
第一种方法失败:v-if和v-for官方文档不建议使用,所以会出现报错
第二种方法失败:使用<component :is=current />,依旧失败,发现是Vue3的bug
第三种方法成功:使用CSS切换内容
切换标签页:用selected标记被选中额标签页(最终选择用title)
-
selected 用index表示,不推荐
-
selected 用name表示,不方便
-
selected 用title表示,有漏洞
由于Vue3不建议用v-if和v-for一起使用,所以就用:class
<component class="gulu-tabs-content-item" :class="{selected: c.props.title === selected }" v-for="c in defaults" :is="c" />
//当c的selected.props.title正好等于selected时,表示它是否展示
动态设置div的高度和宽度
用到的钩子
onMounted/onUpdated/watchEffect
TypeScript 泛型
const indicator = ref <HTMLDivElement> (null)
获取宽高的位置
const { width, left } = el.getBoundingClientRect()
ES6 析构赋值的重命名语法
const { left: left1 } = x.getBoundingClientRect()
const { left: left2 } = y.getBoundingClientRect()
const left = left2 - left1;
代码
Tabs.vue
<template>
<div class="circle-tabs">
<div class="circle-tabs-nav" ref="container">
<div
class="circle-tabs-nav-item"
v-for="(t, index) in titles"
:ref="
(el) => {
if (t === selected) selectedItem = el;
}
"
@click="select(t)"
:class="{ selected: t === selected }"
:key="index"
>
{{ t }}
</div>
<div class="circle-tabs-nav-indicator" ref="indicator"></div>
</div>
<div class="circle-tabs-content">
<component :is="current" :key="current.props.title" />
</div>
</div>
</template>
<script lang="ts">
import Tab from "./Tab.vue";
import { computed, ref, watchEffect, onMounted } from "vue";
export default {
props: {
selected: {
type: String,
},
},
setup(props, context) {
const selectedItem = ref<HTMLDivElement>(null); //被选中的item
const indicator = ref<HTMLDivElement>(null); //移动条
const container = ref<HTMLDivElement>(null); //整个导航
onMounted(() => {
watchEffect(() => {
const { width } = selectedItem.value.getBoundingClientRect();
indicator.value.style.width = width + "px";
const { left: left1 } = container.value.getBoundingClientRect();
const { left: left2 } = selectedItem.value.getBoundingClientRect();
const left = left2 - left1;
indicator.value.style.left = left + "px";
});
});
const defaults = context.slots.default(); //获取插槽内容
defaults.forEach((tag) => {
if (tag.type !== Tab) {
throw new Error("Tabs 子标签必须是 Tab");
}
});
const current = computed(() => {
return defaults.find((tag) => tag.props.title === props.selected);
}); //current等于结算出来的:defaults找到tag.props.title等于被选中的元素
const titles = defaults.map((tag) => {
return tag.props.title;
});
const select = (title: string) => {
context.emit("update:selected", title);
};
return {
current,
defaults,
titles,
select,
selectedItem,
indicator,
container,
};
},
};
</script>
TabsDemo.vue
<template>
<div>Tabs 示例</div>
<h1>示例1</h1>
<Tabs v-model:selected="x">
<Tab title="导航1">内容1</Tab>
<Tab title="导航2">内容2</Tab>
</Tabs>
</template>
<script lang="ts">
import Tabs from "../lib/Tabs.vue";
import Tab from "../lib/Tab.vue";
import { ref } from "vue";
export default {
components: {
Tabs,
Tab,
},
setup() {
const x = ref("导航2");
return {
x,
};
},
};
</script>