笔者由于项目原因需要用element-ui 2实现此效果(如下所示)。本文根据Element-UI 2的el-select和el-tree实现树形下拉选择框的效果,适用于想实现效果但项目组件版本未升级的情形,小白也能看懂!(源码在最后-->)

本文主要参考了下面这位大佬的代码,并在其基础上提出了自己的一些见解:

elementui下拉树形结构【完美实现】by 来干了这碗代码

下面笔者将从组件开始介绍。对组件了解的朋友可以点击标题跳转至代码部分。

1.组件介绍

顾名思义,树形下拉选择框需要用到Tree控件和Select选择器。

 1.1 Select选择器

官方文档:组件 | Select选择器

Select选择器实现的是下拉的效果,由el-select组件包裹着el-option。el-select的主要熟悉为v-model,el-option的主要属性为v-for:'value'

<el-select v-model="value">
    <el-option
      v-for="item in cities"
      :value="item.value">
    </el-option>
  </el-select>

select的v-model属性实现选择器所选节点的显示以及动态绑定的效果。

 option的:value属性绑定的是选项的值,v-for则表示该选项的值从数组中循环(通常数据是以包裹对象的数组的形式传入前端)。

 1.2 Tree树形控件

官方文档:组件 | Tree树形控件

el-tree实现的是树形菜单的效果,其最主要的三个属性分别是data,props属性和node-click事件

<el-tree
  :data="data"
  :props="defaultProps"
  @node-click="handleNodeClick">
</el-tree>

其中,data负责数据的绑定(数据通常是树形数组的形式)。

props负责数据的传递(引用网上的一句解释:“父组件通过 props 向下传递数据给子组件;子组件通过 events 给父组件发送消息”)。

node-click则负责点击节点时的操作(就是click事件套了层皮而已)。

好了,现在你已经了解组件的基本用法了,试着实现树形下拉框的效果吧!

2.代码详解

实现目标效果的方法是select和tree的嵌套,细分下来可有两种方式,分别是option与tree平级(option 1)和option嵌套tree(option 2)。但这两种方式的基本思想是相通的。

<!-- option 1 -->
<el-select>
    <el-option></el-option>
    <el-tree/>
</el-select>

<!-- option 2 -->
<el-select>
    <el-option>
        <el-tree/>
    </el-option>
</el-select>

2.1 option 1

此方法也正是参考文章的博主所采用的方法。

(1)数据处理

示例数据为树形数组。(一般而来说需要自己将后端返回的数据转换为树形数组,示例为了方便省略了这一步,后面会介绍平面数组与树形数组相互转换的方法

cityData: [{
          id: 1,
          label: '重庆',
          children: [{
            id: 2,
            label: '渝北区'
          }]
        }, {...},{...}]

在script模块data()中定义数据。

 (2)模板构造

 a. el-selelct 

绑定v-model="selectValue",selectValue将显示所选节点的值。还需要绑定响应式属性selectTree来实现用户点击不同节点的响应式效果。

b. el-option

el-option是下拉框的选项,每项的属性由v-for循环数组(即方法optionData()的返回值,其中返回值是平面数组)绑定。选项的名称为数组元素的label,值为数组元素的value

c. el-tree

作为目标效果的主体,el-tree需要绑定cityData数据,与之配套的treeProps属性、selectTree响应式属性和handleNodeClick事件。至于:expand-on-click-node="expandOnClickNode",则表示是否只有点击箭头列表才收缩;default-expand-all表示默认展开所有节点。

效果如下图所示。因为采用的是option与tree平级的方法,所以上半部分是下拉框,下半部分是树形目录。

 这个时候只需要给el-option添加display:none样式即可实现目标效果。

 (3)操作绑定

 a. 树形数组转平面数组

在2.1>(2)>b的el-option中,需要用v-for给该组件绑定数据,而我们的数据是树形的,因此需要将树形数组转换为平面数组。参考文章采用的方法仅仅适用于4级数组,也就是最多只能有4级菜单,而且代码冗杂、有很多重复的地方。于是笔者采用迭代的方法实现此目的。

optionData方法的内容很简单,就是迭代加循环。方法有两个参数,一是源数组,二是存放结果的空数组。对源数组进行forEach遍历循环:将当前元素push进结果数组。其中结果数组的label是源数组的label,value是源数组的id。然后再对当前元素进行判断:如果其有children节点,则再次执行optionData方法,而参数则是该元素的children节点和第一次迭代的result。这样直到最后一个元素判断后,将返回result

至于为何要用JSON.parse(JSON.stringify())的方式,是为了防止出现__ob__:observe的而造成无限循环的情况。通过数据深拷贝后,可以去掉数组的__ob__:observe属性。具体可以参考下面这篇文章,里面提到了更多解决方法。

“__ob__: Observer 是 Vue 对数据监控添加的属性。当数据中包含这个属性时,数据是不可枚举、不可遍历的。这里深拷贝的数据应该是我们最终用于赋值的数据,而不是接收到接口的返回值。

参考文章:Vue 去掉数组中的 __ob__ : Observer

最终的结果如下图所示:

 b. 绑定节点事件

在2.1>(2)>c中绑定了事件handleNodeClick。点击节点时,el-select的selectValue属性为当前节点的label值,同时el-tree也会响应式失去焦点。

 我们可以通过console.log检验是否正确。从下面的输出来看是无误的。

 小结一下:

1.由于el-select必须与el-option配套使用,所以可以通过设置display:none的样式隐藏多余的选项;

2.为el-option绑定数据时,因为要用到v-for,因此需要将树形数组转换为平面数组。而为了降低代码的冗余度,故采用迭代的思想来实现效果;

3.vue会为数据增添observe的属性来监听变化,使用深拷贝的方式可以有效去掉,但要注意需要对使用的最终数据进行拷贝而不是返回值。

2.2 option 2

在option 1中,我们对el-option添加了display:none样式来隐藏不必要的选项、还将数组进行了转换,是否有些多此一举了?能否果断一点直接把el-tree包裹在el-option里?因此,笔者尝试用option 2实现此构思。

(1)数据处理

因为不给el-option绑定数据,因此不必对数据做过多的处理。

(2)模板构造

el-select和el-tree的构造与option 1并无大异,主要将el-option的属性全去掉了。

 直接Ctrl+s,效果如下所示,然后报错了。不急,我们一个一个解决。

 a. 解决样式的问题

 首先是下拉框的样式错误。在树形菜单左右两侧都有灰色的背景,并且菜单也没撑满下拉框。我们只需要添加样式让树形菜单撑满即能解决此问题。

 效果如下所示,我们成功解决了!

 b. 解决控制台的报错

由报错信息可知,我们好像缺少一个value的属性。这是因为在使用element-ui时,el-select未绑定v-model或el-option未进行value赋值。显而易见是后者造成的原因。因此只需要添加一个小小的value:' ',即可解决报错。

 大功告成!吧唧吧唧吧唧。

 小结两下:

1. 也可以el-tree嵌套在el-option,这样有两个好处:一是更易于理解,毕竟是将下拉的树形菜单;二是减少没有必要的代码,包括数组的转换,数据的绑定;

2. 造成Missing required prop: “value”的原因通常由是el-select或el-tree造成。

3. Element-UI 3新增组件实现

是的,element-ui 3中新增了el-tree-select组件来实现树形下拉框。

官方文档:组件 | TreeSelect 树形选择

因为此组件是由el-tree和el-select结合而来,并未修改原有属性,故组件的属性和事件也是两者结合。

4. 源码分享

<template>
  <div class="app-container">
    <el-select 
              class="main-select-tree" 
              ref="selectTree" 
              v-model="selectValue" 
              style="width: 300px;"
              name='option 1'>

      <!-- otion 1 -->
      <el-option 
                v-for="item in optionData(cityData)" 
                :label="item.label" 
                :value="item.value"
                style="display: none;"/>

      <el-tree 
              class="main-select-el-tree" 
              ref="selectelTree" 
              :data="cityData" 
              :props='treeProps'
              highlight-current 
              @node-click="handleNodeClick"
              :expand-on-click-node="expandOnClickNode" 
              default-expand-all />

      <!-- option 2 -->
      <!-- <el-option style="height: 100%; padding: 0;" value="">
          <el-tree
                  class="main-select-el-tree"
                  ref="selectelTree" 
                  :data="cityData"
                  :props='treeProps'
                  @node-click="handleNodeClick"
                  :expand-on-click-node="expandOnClickNode"
                  highlight-current
                  default-expand-all
                  style="font-weight: normal;"/>
        </el-option> -->
    </el-select>
  </div>
</template>
  
<script>
export default {
  data() {
    return {
      selectValue: '',
      expandOnClickNode: true,
      options: [],
      treeProps: {
        children: 'children',
        label: 'label'
      },
      cityData: [{
        id: 1,
        label: '重庆',
        children: [{
          id: 2,
          label: '渝北区'
        }]
      }, {
        id: 3,
        label: '北京',
        children: [
          { id: 4, label: '海淀区' },
          { id: 5, label: '朝阳区' }
        ]
      }, {
        id: 6,
        label: '四川',
        children: [
          {
            id: 7,
            label: '成都',
            children: [
              { id: '8', label: '成华区' }
            ]
          }
        ]
      }]
    }
  },

  methods: {
    /**
     * 树形转平面的迭代方法
     * option 1的el-option需要此方法绑定数据
     */
    optionData(array, result=[]) {
      array.forEach(item => {
        result.push({label:item.label,value:item.id})
        if (item.children && item.children.length !== 0) { 
          this.optionData(item.children, result)
        } 
      })
      return JSON.parse(JSON.stringify(result))
    },

    // 点击节点的响应
    handleNodeClick(node) {
      this.selectValue = node.label;
      this.$refs.selectTree.blur();
      console.log(node.label);
    }
  }
}
</script>

<style>
.main-select-el-tree .el-tree-node .is-current>.el-tree-node__content {
  font-weight: bold;
  color: #409eff;
}

.main-select-el-tree .el-tree-node.is-current>.el-tree-node__content {
  font-weight: bold;
  color: #409eff;
}
</style>

因为笔者刚开始接触前端,如果文章中有错误恳请指正。如果这篇文章帮到你,不妨点个小心心支持一下笔者,阿里嘎多~


2023.6.1 更新

关于扁平数组与数据数组转换的方法已更新,感兴趣的朋友可以看我另一篇文章💖

扁平数组与树形数组的相互转换(详例 & 代码)--HelloWord精通

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐