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--(构建树形菜单)的主要内容,如果未能解决你的问题,请参考以下文章

树形控件如何隐藏vue

基于vue+elementUI基础写的横向树形组件

vue实现树形tree展示组件封装

vue+element tree(树形控件)组件

vue递归组件 (树形控件 )

vue实现树形tree展示组件封装