vue递归组件—开发树形组件Tree--(构建树形菜单)
Posted 我总是词不达意
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue递归组件—开发树形组件Tree--(构建树形菜单)相关的知识,希望对你有一定的参考价值。
在 Vue 中,组件可以递归的调用本身,但是有一些条件:
- 该组件一定要有
name
属性- 要确保递归的调用有终止条件,防止内存溢出
不知道大家有没遇到过这样的场景:渲染列表数据的时候,列表的子项还是列表。如果层级少尚且可以用几个for循环搞定,但是层级多或者层级不确定就有点无从下手了。
其实这就是树形结构数据,像常见的例如导航、空间或逻辑组织、页面定位、级联选择等,其结构可展开或折叠,都属于这种结构。
效果展示
以上就是使用组件递归,并加入简单交互的展示效果。点击节点会在控制台输出节点对应的数据,如果有子节点,则会展开或收起子节点。接下来我们就看看如何实现以上效果吧!
渲染完整数据
渲染数据这一步非常简单,首先是把树形结构封装成一个列表组件,其次判断每一项有没有子节点,如果有子节点,再使用自身组件去渲染就可以了。
递归组件:src/components/tree-folder.vue
//项目用到vant-ui库
<template>
<div class="tree-item">
<div v-for="item in treeData" :key="item.id">
<div class="item-title">
<span v-text="item.name"></span>
<span v-if="item.children && item.children.length">
//vant组件库图标 看个人需求换成自己需要的
<van-iconname="arrow-down"/>
</span>
</div>
<div v-if="item.children && item.children.length" class="item-childen">
<tree-folder :treeData="item.children"></tree-folder>
</div>
</div>
</div>
</template>
<script>
export default
name: 'TreeFolder',
props:
treeData:
type: Array,
default: () => []
</script>
<style lang="stylus">
.tree-item
.item-title
padding: 4px 8px
.name
color: #000
.negative-rotate
transform: rotate(-180deg)
.item-childen
padding-left: 20px
</style>
父组件: src/home.vue
<template>
//引用递归组件,并传递数据
<TreeFolder :treeData="treeData" />
</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 TreeFolder from '@/components/tree-folder.vue'
export default
components:
TreeFolder
,
data()
return
treeData: treeData
</script>
效果如下
获取节点数据
接下来我们要做的是,点击节点时在控制台输出对应的数据。首先我们使用
$emit
,将一级节点的 item 传递出去,也就是子传父的方法,相信大家都会。
其次是将内层节点的数据传递出去,同样使用子传父的方法,只是我们需要给组件里面的 tree-folder绑定
@tree-node-click="$emit('tree-node-click', $event)"
,这样每次子级每次都可以调用父级的 tree-node-click 方法,父级又调用它的父级 tree-node-click 方法,最终调的都是最外层的 tree-node-click 方法,我们只需要在这个过程中,把数据传递过去就可以了。这块有点绕,相信大家多看几遍应该可以看懂。修改如下:
递归组件:src/components/tree-folder.vue
//方法名可以自取 不一定非是'tree-node-click'
<div class="item-title">
<span v-text="item.name"></span>
<span v-if="item.children && item.children.length">
//vant组件库图标 看个人需求换成自己需要的
<van-icon name="arrow-down"/>
</span>
</div>
<div v-if="item.children && item.children.length" class="item-childen">
<tree-folder
:treeData="item.children"
@tree-node-click="$emit('tree-node-click', $event)"
></tree-folder>
</div>
...
methods:
itemNodeClick(item)
this.$emit('tree-node-click', item)
父组件: src/home.vue
<TreeFolder:tree-data="treeData" @tree-node-click="nodeClick" />
...
methods:
nodeClick(val)
console.log(val)
效果如下
动态展开收起并给点击项添加激活样式和设置图标的样式
动态展开收起的思路是给组件设置一个数组,数组中存放的是当前列表中需要展开的节点的id,当点击节点的时候添加或删除节点id,然后判断每个节点的id在不在这个数组,在则显示子节点,不在则隐藏子节点。
点击项添加激活样式的思路是给组件绑定一个变量,变量存放的值是点击当前列表中节点的id,当点击节点的时候通过id添加相对应的样式。
图标样式的思路是根据数组中存放的的节点的id,判断节点的id如果在这个数组,则旋转 -180deg,不在则回到最初状态。
递归组件:src/components/tree-folder.vue
//项目用到vant-ui库
<template>
<div class="tree-item">
<div v-for="item in treeData" :key="item.id">
<div class="item-title">
<span
:class=" 'fc-theme': item.id == curNameId " // fc-theme激活样式的类名
v-text="item.name">
</span>
<span v-if="item.children && item.children.length">
//vant组件库图标 看个人需求换成自己需要的
<van-icon
name="arrow-down"
style="transition: transform 0.3s" // 加一个延迟动画效果
:class=" 'negative-rotate': isOpen(item.id), 'fc-theme': item.id ==
curNameId "/> // 根据id判断是否旋转图标和添加激活样式
</span>
</div>
<div v-if="item.children && item.children.length" v-show="isOpen(item.id)"
class="item-childen">
//这里是重点:current="curNameId",否则无法实现动态添加激活样式
<tree-folder :treeData="item.children" :current="curNameId" @tree-node-
click="$emit('tree-node-click', $event)">
</tree-folder>
</div>
</div>
</div>
</template>
<script>
export default
name: 'TreeFolder',
props:
treeData:
type: Array,
default: () => []
,
current: Number //保存当前点击节点的id
,
data()
return
expandedKeys: [], // 当前列表需要展开的节点id组成的数组
curNameId: 0 //保存current的值
,
computed:
isOpen()
return function (id)
// 判断节点id在不在数组中,在则显示,不在则隐藏
return this.expandedKeys.includes(id)
,
watch:
//监听当前点击节点的id
current:
handler(num)
this.curNameId = num
,
deep: true,
immediate: true
,
methods:
itemNodeClick(item)
this.$emit('tree-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)
</script>
<style lang="stylus">
.tree-item
.item-title
padding: 4px 8px
.name
color: #000
.negative-rotate
transform: rotate(-180deg)
.item-childen
padding-left: 20px
</style>
父组件: src/home.vue
<TreeFolder:tree-data="treeData" :current="curNameId" @tree-node-click="nodeClick" />
...
data()
return
curNameId: 0
,
methods:
nodeClick(val)
this.curNameId = val.id
this.$set(val, 'curNameId', this.curNameId)
最终效果
附上一个疑问的解答
组件调用组件自己的时候,$emit了tree-node-click事件,这个功能主要是做什么的,还有为什么$emit('tree-node-click', $event)第二个参数是$event而不是一个可变数据
这是为了实现内层的子传父功能,使用了@tree-node-click="$emit('tree-node-click', $event)",在内层执行this.$emit("tree-node-click", item) 时候,其实就是执行父组件的 $emit('tree-node-click', $event),然后又会执行爷爷组件的 $emit('tree-node-click', $event),最终执行的是父组件: src/home.vue 的 nodeClick 事件,其实参数一直是item。
以上就是今天的分享!是有点绕,请耐心看完思考并且自己动手试一试。有兴趣的小伙伴可以动手试一哈,把组件进一步封装,或修改成自己想要的样式和效果。
vue递归组件 (树形控件 )
首先我们要知道,既然是递归组件,那么一定要有一个结束的条件,否则就会使用组件循环引用,最终出现“max stack size exceeded”的错误,也就是栈溢出。那么,我们可以使用v-if="判断条件"作为递归组件的结束条件。当遇到v-if为false时,组件将不会再进行渲染
1. 准备一个树状的递归数据
navigation: [ { types: 1, id: "0", name: "首页", path: "/jiaowu_system/home", icon: "icon_hrIndex.png", children: [] }, { types: 1, id: "1", name: "教学资源", path: "", icon: "jiaowu_system_jiaoxueziyuan.png", children: [ { types: 2, id: "1 - 1", name: "学校信息", path: "/jiaowu_system/SchoolInformation", icon: "", children: [] }, { types: 2, id: "1 - 2", name: "管理部门信息", path: "/jiaowu_system/administration", icon: "", children: [] }, { types: 2, id: "1 - 3", name: "专业信息", path: "/jiaowu_system/Ammatilliset", icon: "", children: [] }, { types: 2, id: "1 - 4", name: "教学场地信息", path: "/jiaowu_system/classroom", icon: "", children: [] } ] }, { types: 1, id: "2", name: "教学计划", path: "", icon: "jiaowu_system_jihua.png", children: [ { types: 2, id: "2 - 1", name: "课程环节/管理", path: "/jiaowu_system/CoursePractice", icon: "", children: [] }, { types: 2, id: "2 - 2", name: "设置教学计划", path: "", icon: "", children: [ { types: 3, id: "2 - 1 - 1", name: "设置培养方案", path: "/jiaowu_system/trainingPlan", icon: "", children: [] }, { types: 3, id: "2 - 1 - 2", name: "复制培养方案", path: "/jiaowu_system/copyTrainingPlan", icon: "", children: [] } ] }, { types: 2, id: "2 - 3", name: "核定教学计划", path: "/jiaowu_system/ensure_plan", icon: "", children: [] }, { types: 2, id: "2 - 4", name: "查看教学计划", path: "/jiaowu_system/teachingResources", icon: "", children: [] } ] }
]
2. 创建一个简单的递归组件
// 这里 我是通过name实现递归效果的 你可以把它当作从import导入了一个组件并注册,我们在temlpate可以使用<list-menu></list-menu>使用子组件自身进行递归了 默认不展示子组件,只能在父组件点击的时候才会展示 使用的 v-show 减少渲染消耗
<template> <div class="list"> <div @click.prevent="handleClick"> {{ model.name }} </div> <div v-show="reveal" v-if="isDispaly" style="margin-left:20px;"> <list-menu v-for="item in model.children" :key="item.id" :model="item" /> </div> </div> </template> <script> export default { name: "listMenu", components: {}, props: ["model"], data() { return { reveal: false }; }, methods: { handleClick() { if (this.isDispaly) { this.reveal = !this.reveal; } } }, computed: { isDispaly() { return this.model.children && this.model.children.length; } } }; </script> <style scoped> div { width: 100px; margin: 20px 0; } </style>
上述代码中我们需要注意,这个组件必须含有 name 这个属性,因为没有 name 这个属性会造成控件自身不能调用自身, 当使用它时,只需要把上边我们定义好的数据通过props的方式传进去即可
3. 我们创建一个sidebar组件,这个组件作为使用递归组件的父组件
// navigation的数据在上面 需要copy
<template> <div class="sidebar"> <div v-for="menu in navigation" :key="menu.id"> <list-menu :model="menu"></list-menu> </div> </div> </template> <script> import listMenu from "./list"; export default { name: "sidebar", components: { listMenu }, props: {}, data() { return { navigation: [] // 数据太长 就不在这里面写了 }; } }; </script>
好了 我们就实现了一个简单的递归侧边栏组件,这段代码只是简单的做了下递归组件的使用。对于折叠树状菜单来说,我们一般只会去渲染一级的数据,当点击一级菜单时,再去渲染一级菜单下的结构,如此往复。那么v-if就可以实现我们的这个需求,当v-if设置为false时,递归组件将不会再进行渲染,设置为true时,继续渲染。组件中的name不仅可以递归的时候使用 还可以当项目使用keep-alive时,可搭配组件name进行缓存过滤
一个简单的小实例
<div id="app"> <keep-alive exclude="Detail"> <router-view/> </keep-alive> </div>
以上是关于vue递归组件—开发树形组件Tree--(构建树形菜单)的主要内容,如果未能解决你的问题,请参考以下文章