练习2:创建一个标签页组件
Edit me
组件的功能:
所有的信息都在index.html
提供,包括:
1 | 默认选中标签值 (activeKey) | 写入index.html 的Vue实例的数据中,通过v-model 进行双向数据传输 |
2 | 标签文字 | 绑定在pane 组件标签上,传递给pane 组件 |
3 | 标签 ID | 绑定在pane 组件标签上,传递给pane 组件 |
4 | 标签正文 | 直接写入组件标签<pane> 中间,所以需要在pane 组件中进行开槽 |
5 | 为<pane> 标签开槽 |
<pane> 组件标签插入在<tabs> 组件标签中,所以同样需要在tabs 组件的模板中开槽 |
步骤一:index.html
的DOM
结构
index.html
的DOM
结构,以及创建 Vue 实例,挂载上去。
<div id="app">
<!-- 1-->
<tabs v-model="activeValue">
<!-- 2 & 3 & 4-->
<pane label="标签一" name="1">标签一的内容</pane>
<pane label="标签一" name="2">标签二的内容</pane>
<pane label="标签一" name="3">标签三的内容</pane>
</tabs>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
activeKey: '1'
}
})
</script>
步骤二:为<pane>
标签在tabs
组件上开槽
Vue.component('tabs', {
template: '\
<div class="tabs"> \
<div class="tabs-bar"> \
<!-- 标签标题的所在 --> \
</div> \
<div class="tabs-content"> \
<!-- 5 为<pane>标签开槽 -->\
<slot></slot> \
</div> \
</div>',
data: function() {
return {
// 将接收到的props数据保存到组件自身的数据中,进行修改
currentValue: this.value
}
},
props: {
// 接收上层通过`v-model`传递过来的数据
value: {
type: [String, Number]
}
}
})
步骤三:为标签正文内容在pane
组件上插槽
/*
pane-a. 在`pane`组件中开槽,从而让组件标签中插入的内容——标签对应的正文内容直接渲染到插槽中
pane-b. 用一个v-show来控制组件实例是否显示
pane-c. props接收上层组件传递过来的数据
*/
// pane-a & pane-b
Vue.component('pane', {
// name属性会在步骤四-遍历tabs的children组件时需要用到
name: 'pane',
template: '\
<div class="pane" v-show="show"> \
<slot></slot>\
</div>\',
data: function() {
return {
show: true
}
},
// pane-c
props: {
label: {
type: String,
default: ''
},
name: {
type: String
}
}
})
步骤四:在tabs
组件通过子链$children
建立标签的数组
//////////////tabs//////////////
Vue.component('tabs', {
...,
methods: {
getTabs() {
// 需要通过高阶函数来找到tabs下所有的pane组件,高阶函数中的`this`不再指向Vue实例,因此需要在它的外部定义一个局部变量,指向Vue实例
var _this = this;
return this.$children.filter(function(item) {
// 通过$options可以访问Vue实例的选项的键值
item.$options.name === 'pane';
})
}
}
})
步骤五:根据currentValue
,设定<pane>
的show
属性,从而控制pane
实例显示与否
Vue.component('tabs', {
...,
methods: {
...,
updateStatus() {
// 返回所有的pane组件
var tabs = this.getTabs();
var _this = this;
tabs.forEach(function(tab) {
return tab.show = tab.name === _this.currentValue;
})
}
}
})
步骤六:在tabs
组件的navList
数据中添加标签对象,并进行渲染
6-1 在tabs
组件中创建一个数组,会包含不同的标签对应的对象格式的元素,囊括了每个标签的label
和name
数据
Vue.component('tabs', {
...,
data: function() {
return {
navList: []
}
},
methods: {
updateNav() {
this.navList = [];
var _this = this;
this.getTabs.forEach(function(pane, index) {
_this.navList.push({
label: pane.label,
// 如果有name信息,那么就用name信息,否则就直接用索引值
name: pane.name || index
});
// 问题,我的理解是下面这一句和上面的没有区别
if (!pane.name) pane.name = index;
// 如果之前没有通过v-model传入value,从而定义默认的currentValue,那么就默认让index为0的那个标签成为默认值
if (index === 0) {
if (!_this.currentValue) {
_this.currentValue = pane.name || index;
}
}
});
// 根据currentValue设置每个pane实例属性show的布尔值
updateStatus();
}
}
})
6-2 渲染标签
需要做三件事情:
-
使用
v-for
对navList
进行遍历,将item.label
渲染出来 -
用数组语法赋予
tabs
classes -
将点击的那一个标签的
name
值赋值给currentValue
,并释放事件到上层,通知更改。
Vue.component('tabs', {
tempalte: '\
<div class="tabs> \
<div class="tabs-bar"> \
<div \
:class="tabCls(item)" \
:v-for="(item, index) in navList" \
@click="handleChange(index)" \
></div>\
</div> \
<div class="tabs-content"> \
<slot></slot> \
</div> \
</div>',
...
})
6-2-1 使用数组语法绑定标签class
属性
Vue.component('tabs', {
...,
methods: {
tabCls(item) {
return [
'tabs-tab',
{
'tabs-tab-active': item.name === this.currentValue;
}
]
}
}
})
6-2-2 点击事件更新currentValue
值,并通知上层组件
Vue.component('tabs', {
...,
methods: {
handleChange(index) {
var nav = navList[index];
var name = nav.name
this.currentValue = name;
this.$emit('input', name);
// 这个on-click是由谁捕捉的,什么用?之前练习1的on-change也类似,也不是很明白什么作用
this.$emit('on-click', name);
}
}
})
步骤 7:Watch (不懂watch的时机)
步骤 7-1:tabs
组件的 watch
watch 两个值:
-
上层组件通过
v-model
传递过来的数据,也就是value
-
自身的
currentValue
的值
Vue.component('tabs', {
...,
watch: {
value: function(val) {
this.currentValue = val;
},
// 自身currentValue变化时,需要更新所有下层`pane`组件的show属性的布尔值,从而对 DOM 进行重新渲染
currentValue: function() {
this.updateStatus();
}
}
})
步骤7-2:pane
组件的 watch
随着每次点击事件,都要伴随label
的更新,可以直接通过父链$parent来调用父组件的方法
Vue.component('pane', {
...,
methods: {
updateNav() {
this.$parent.updateNav();
}
},
watch: {
label() {
this.updateNav();
}
},
mounted: {
this.updateNav();
}
})