这样的场景:渲染列表数据的时候,列表的子项还是列表。如果层级少尚且可以用几个 for 循环搞定,但是层级多或者层级不确定就有点无从下手了。
其实这就是树形结构数据,像常见的组织架构图,文件夹目录,导航菜单等都属于这种结构。很多组件库都带有树形组件,但往往样式不是想要的,改起来也非常的费劲。那么,如何自己渲染这些数据呢?答案就是——组件递归!
效果展示
以上就是使用组件递归,并加入简单交互的展示效果。点击节点会在控制台输出节点对应的数据,如果有子节点,则会展开或收起子节点。接下来就看看如何实现以上效果吧!
渲染完整数据
渲染数据这一步非常简单,首先是把树形结构封装成一个列表组件,其次判断每一项有没有子节点,如果有子节点,再使用自身组件去渲染就可以了。
src/components/myTree.vue
<template>
<div class="tree-item">
<div v-for="item in treeData" :key="item.id">
<div class="item-title">{{ item.name }}</div>
<div v-if="item.children && item.children.length" class="item-childen">
<my-tree :treeData="item.children"></my-tree>
</div>
</div>
</div>
</template>
<script>
export default {
name: "myTree",
props: {
treeData: {
type: Array,
default: () => [],
},
},
};
</script>
<style lang="scss" scoped>
.tree-item {
.item-title {
padding: 4px 8px;
}
.item-childen {
padding-left: 20px;
}
}
</style>
src/App.vue
<template>
<my-tree :tree-data="treeData"></my-tree>
</template>
<script>
const treeData = [
{ id: 1, name: "一级1" },
{
id: 2,
name: "一级2",
children: [
{ id: 3, name: "二级2-1" },
{ id: 4, name: "二级2-2" },
],
},
{
id: 5,
name: "一级3",
children: [
{
id: 6,
name: "二级3-1",
children: [
{ id: 7, name: "三级3-1-1" },
{ id: 8, name: "三级3-1-2" },
],
},
{ id: 9, name: "二级3-2" },
{ id: 10, name: "二级3-3" },
],
},
];
import myTree from "@/components/myTree.vue";
export default {
components: {
myTree,
},
data() {
return {
treeData: treeData,
};
},
};
</script>
效果如下
获取节点数据
接下来要做的是,点击节点时在控制台输出对应的数据。首先使用 $emit,将一级节点的 item 传递出去,也就是子传父的方法。
其次是将内层节点的数据传递出去,同样使用子传父的方法,只是需要给组件里面的 my-tree 绑定@node-click="$emit('node-click', $event)",这样每次子级每次都可以调用父级的 node-click 方法,父级又调用它的父级 node-click 方法,最终调的都是最外层的 node-click 方法,只需要在这个过程中,把数据传递过去就可以了。修改如下:
src/components/myTree.vue
<div class="item-title" @click="itemNodeClick(item)">{{ item.name }}</div>
<div v-if="item.children && item.children.length" class="item-childen">
<my-tree
:treeData="item.children"
@node-click="$emit('node-click', $event)"
></my-tree>
</div>
// ...
itemNodeClick(item) {
this.$emit("node-click", item)
}
src/App.vue
<my-tree :tree-data="treeData" @node-click="nodeClick"></my-tree>
// ...
nodeClick(val) {
console.log(val)
}
效果如下
动态展开收起
这一步的思路是给组件设置一个数组,数组中存放的是当前列表中需要展开的节点的 id,当点击节点的时候添加或删除节点 id,然后判断每个节点的 id 在不在这个数组,在则显示子节点,不在则隐藏子节点。
src/components/myTree.vue
<div class="item-title" @click="nodeClick(item)">
<span>{{ item.name }}</span>
<span v-if="item.children && item.children.length">
[{{ isOpen(item.id) ? '-' : '+' }}]
</span>
</div>
<div
v-if="item.children && item.children.length"
v-show="isOpen(item.id)"
class="item-childen"
>
<my-tree
:treeData="item.children"
@node-click="$emit('node-click', $event)"
></my-tree>
</div>
// ...
data() {
return {
expandedKeys: [] // 当前列表需要展开的节点id组成的数组
}
},
methods: {
nodeClick(item) {
this.$emit('node-click', item)
if (item.children && item.children.length) {
let index = this.expandedKeys.indexOf(item.id)
if (index > -1) {
// 如果当前节点id存在数组中,则删除
this.expandedKeys.splice(index, 1)
} else {
// 如果当前节点id不存在数组中,则添加
this.expandedKeys.push(item.id)
}
}
},
isOpen(id) {
// 判断节点id在不在数组中,在则显示,不在则隐藏
return this.expandedKeys.includes(id)
}
}
效果如下
最后再添加一些样式,就大功告成
完整代码
src/components/myTree.vue
<template>
<div class="tree-item">
<div v-for="item in treeData" :key="item.id">
<div class="item-title" @click="nodeClick(item)">
<span>{{ item.name }}</span>
<span v-if="item.children && item.children.length">
[{{ isOpen(item.id) ? "-" : "+" }}]
</span>
</div>
<div
v-if="item.children && item.children.length"
v-show="isOpen(item.id)"
class="item-childen"
>
<my-tree
:treeData="item.children"
@node-click="$emit('node-click', $event)"
></my-tree>
</div>
</div>
</div>
</template>
<script>
export default {
name: "myTree",
props: {
treeData: {
type: Array,
default: () => [],
},
},
data() {
return {
expandedKeys: [], // 当前展开的节点id组成的数组
};
},
methods: {
nodeClick(item) {
this.$emit("node-click", item);
if (item.children && item.children.length) {
let index = this.expandedKeys.indexOf(item.id);
if (index > -1) {
// 如果当前节点id存在数组中,则删除
this.expandedKeys.splice(index, 1);
} else {
// 如果当前节点id不存在数组中,则添加
this.expandedKeys.push(item.id);
}
}
},
isOpen(id) {
// 判断节点id在不在数组中,在则显示,不在则隐藏
return this.expandedKeys.includes(id);
},
},
};
</script>
<style lang="scss" scoped>
.tree-item {
cursor: pointer;
.item-title {
padding: 4px 8px;
&:hover {
background: #eee;
}
}
.item-childen {
padding-left: 20px;
}
}
</style>
src/App.vue
<template>
<my-tree :tree-data="treeData" @node-click="nodeClick"></my-tree>
</template>
<script>
const treeData = [
{ id: 1, name: "一级1" },
{
id: 2,
name: "一级2",
children: [
{ id: 3, name: "二级2-1" },
{ id: 4, name: "二级2-2" },
],
},
{
id: 5,
name: "一级3",
children: [
{
id: 6,
name: "二级3-1",
children: [
{ id: 7, name: "三级3-1-1" },
{ id: 8, name: "三级3-1-2" },
],
},
{ id: 9, name: "二级3-2" },
{ id: 10, name: "二级3-3" },
],
},
];
import myTree from "@/components/myTree.vue";
export default {
components: {
myTree,
},
data() {
return {
treeData: treeData,
};
},
methods: {
nodeClick(val) {
console.log(val);
},
},
};
</script>
效果如下