vue 自定义标签页
本篇文章我们通过以前学习过的知识来构建一个自己的标签页组件。首先我们创建一个基于vue的项目,我用的IDE是webstorm。创建好的工程目录结构:我们先来分析一下我们需要做哪些工作,常见的标签页就像下面的图片:大家可以看到标签页的最上边是我们的标题区域,而下面则是我们对应的内容区域。第一眼看到这个布局,我就在想这个好像没有什么难度的,标题栏我用v-for渲染出来,下面的内容有几个...
本篇文章我们通过以前学习过的知识来构建一个自己的标签页组件。
首先我们创建一个基于vue的项目,我用的IDE是webstorm。
创建好的工程目录结构:
我们先来分析一下我们需要做哪些工作,常见的标签页就像下面的图片:
大家可以看到标签页的最上边是我们的标题区域,而下面则是我们对应的内容区域。
第一眼看到这个布局,我就在想这个好像没有什么难度的,标题栏我用v-for渲染出来,下面的内容有几个标签我就用几个div来展示,无非控制一下显示和隐藏就好了。这么设计没有任何的毛病,不过这样的话我们就显示不出来我们组件的价值了。难道加一个标签,我还要修改里面的代码?
我们在仔细分析一下,不管有几个标签,我们都能正确的渲染出来,而不是和第一个设计一样,我在加个div来实现。那么下面的内容区域必然要和我们的组件抽离,也就是说我们要实现的tabs标签页内部应该是嵌套的。那么在最外层的组件tabs就应该有标题栏和一个slot用来展示对应的内容。而slot中的内容就是具体的要展示的内容,由于这部分也是动态的,可想而知应该也是一个组件内部预留slot来实现的。那么,就按照这个思路我们来看下能不能实现这样的功能。
我们这里就将这两部分分别命名为:tabs和panel组件,我们在工程中首先创建这两个文件。
在创建好的工程基础上,我们将HelloWorld的文件内容清除,暂时不展示任何东西,方便我们待会使用我们的组件tabs。
接下来,我们就定制一下我们tabs和panel的代码。
Tabs:
<template>
<div class="tabs">
<div class="tabs-bar">
<!--标签页标题,我们使用v-for进行渲染-->
</div>
<div class="tabs-content">
<!--使用slot来进行panel嵌套-->
<slot></slot>
</div>
</div>
</template>
Panel:
<template>
<div class="tabs-panel">
<slot></slot>
</div>
</template>
我们使用Tabs组件的姿势应该是这样子的:
<template>
<Tabs>
<Panel></Panel>
<Panel></Panel>
<Panel></Panel>
</Tabs>
</template>
<script>
import Tabs from "@/components/Tabs";
import Panel from "@/components/Panel";
export default {
name: 'HelloWorld',
components: {Panel, Tabs},
comments: {
'Tabs': Tabs,
'Panel': Panel,
}
}
</script>
然后运行程序:yarn run dev 或者 npm run dev
大家可以从右边的源码可以看到,我们想要的结构已经渲染出来了,那么接下来就是为我们的组件填充内容和控制逻辑了。
首先我们要显示的就是标题了,每个标签页都有自己的标题,而这个标题应该在Panel组件中定义,Tabs组件的初始化的时候获取然后显示。
我们在Panel组件中首先定义标签页的标题,显然这是一个props,用户可以动态设置的。
另外我们还需要给panel指定一个唯一标识和控制panel显示隐藏的变量。随大流,我们分别将这几个变量命名为:label,name,visible。其中name可以由用户指定,如果没有指定那么我们可以按照从0开始指定。这些操作都是由Tabs完成。
<template>
<div class="tabs-panel" v-show="visible">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Panel',
data() {
return {
visible: true,
}
},
props: {
name: {
type: String
},
label: {
type: String,
default: ''
}
},
}
</script>
<style scoped>
</style>
那么这应该就是panel修改后的代码了。
我们先不考虑Tabs怎么处理这些东西,我们先将Tabs使用Panel的格式写出来:
<Tabs>
<Panel label="标签1">
我是标签1
</Panel>
<Panel label="标签2">
我是标签2
</Panel>
<Panel label="标签3">
我是标签3
</Panel>
</Tabs>
这里我们不指定name值,最终的样子应该如上。此时运行程序的结果是:
这里和我们看的标签页有哪些不同呢?
1.标签页标题栏没有展示
2.标签页的内容全部显示出来了
在处理Tabs的工作之前,我们在想一下Panel组件还有什么需要处理的。
大家回想一下,Panel中的label是用户动态设置的,如果label在初始化或者发生变化的时候是不是应该通知父组件也就是Tabs组件来更新标签页的标题?
mounted() {
this.updateNav()
},
watch: {
label() {
this.updateNav()
}
},
methods: {
updateNav() {
this.$parent.updateNav()
},
}
我们添加相应的方法,暂时我们就取名叫updateNav。其中Tabs和Panel是两个独立的组件,我们要访问父组件,就必须通过父链来操作也就是 this.$parent。
在这里我们在初始化和label内容发生变化的时候通知父组件来更新标签页标题。
接下来的工作应该是Tabs的实现了。我们首先要实现的是,在Tabs初始化的时候获取所有的Panel子组件并且渲染内容。
那么我们就首先来获取Tabs下的子组件:
methods:{
getTabs(){
console.log(this.$children)
}
}
这些我们先定义一个方法getTabs用来获取子组件。大家应该还记得在vue中可以通过this.$children来获取下面的子组件。我们先来看下输出结果:
大家可以看到返回的是一个数组对象,那么我们在仔细看下里面的内容:
这里我们需要用到的是options的数据,先来看下官方文档中对于它的解释:
也就是说我们可以通过vm.$options的形式获取到我们的自定义属性,这里就是:label和name。
那么我们就知道该怎么从Panel组件获取我们想要的数据了:
getTabs() {
return this.$children.filter(item=>{
return item.$options.name == 'Panel'
})
},
首先对getTabs进行改造,这里我们需要的是Panel组件,其他的不是我们Tabs的有效子组件,所以进行一下数据过滤。
接下来我们考虑一下我们还需要什么?
首先,我们在Panel初始化完成后,会调用Tabs组件的updateNav方法,在这个方法中我们需要获取子组件的label和name信息来初始化标签页标题,然后我们应该还有一个变量来存储这些数据以便Tabs来渲染标题。
其次,我们需要知道哪个Panel或者是标签处于选中状态,这一点我们可以定义一个变量标识当前操作的Panel,显然这个值是Panel的name属性。
在知道当前是哪个标签页后,我们需要给标签页的标签动态增加一个类比如给这个标签添加一个边框等等来区分未选中的标签,同时我们还需要控制对应的Panel内容的显示隐藏。
我们按照上面的思路最终写出的代码如下:
<template>
<div class="tabs">
<div class="tabs-bar">
<!--标签页标题,我们使用v-for进行渲染-->
<div :class="tabCls(item)"
v-for="(item,index) in navList"
:key="index"
@click="handleChanged(index)">
{{item.label}}
</div>
</div>
<div class="tabs-content">
<!--使用slot来进行panel嵌套-->
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: 'Tabs',
data() {
return {
current: this.value || 0,
navList: [],
}
},
props: {
value: [String, Number]
},
mounted() {
console.log(this.getTabs())
},
watch: {
value(val) {
this.current = val
},
current(val) {
// 当前tab发生变化时,更新panel的显示状态
this.updateStatus()
}
},
methods: {
// 当tab选中时,动态添加样式
tabCls(item) {
return [
'tabs-tab',
{'tabs-tab-active': item.name === this.current}
]
},
// 获取panel子组件
getTabs() {
return this.$children.filter(item => {
return item.$options.name == 'Panel'
})
},
// 显示对应的panel组件内容,并隐藏未被选中的panel
updateStatus() {
this.getTabs().forEach(panel => {
panel.visible = panel.name === this.current
})
},
// 更新tabs
updateNav() {
this.navList = []
this.getTabs().forEach((panel, index) => {
this.navList.push({
label: panel.label,
name: panel.name || index
})
// 如果panel没有定义name属性,那么使用index作为panel标识
if (!panel.name) {
panel.name = index
}
// 设置第一个panel为当前活动panel
if (index === 0) {
if (!this.current) {
this.current = panel.name || index
}
}
this.updateStatus()
})
},
// 点击标签页标题时触发
handleChanged(index) {
// 修改当前选中tab
this.current = this.navList[index].name
// 更新value
this.$emit('input', this.current)
// 触发自定义事件,供父级使用
this.$emit('on-click', this.current)
}
}
}
</script>
<style scoped>
</style>
这里我们使用value用来指定当前活动的tab,其中 this.$emit(‘input’, this.current)是为了实现v-model这个相关的知识大家应该都了解。
另外一个注意点就是,动态添加类的时候,应为计算属性是无法接受参数的,所以我们不知道哪个tab处于选中状态,所以这里使用methods,methods是不缓存的,每次发生变化都会重新调用的。
current: this.value || 0,
这句代码主要是为了,如果我们没有使用v-model,仍然可以让Tabs保持第一个选中,当然了,还是有小问题的,如果Panel设置name属性会怎么样,这里就不说了,大伙可以试试。这是我们没有添加样式的结果:
好了,最后当然是给它一个像样子的样式,至少能达到能看的地步:
更多推荐
所有评论(0)