Henry Henry
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)

Henry

小学生中的前端大佬
  • JavaScript
  • TypeScript
  • Vue
  • ElementUI
  • React
  • HTML
  • CSS
  • 技术文档
  • GitHub 技巧
  • Nodejs
  • Chrome
  • VSCode
  • Other
  • Mac
  • Windows
  • Linux
  • Vim
  • VSCode
  • Chrome
  • iTerm
  • Mac
  • Obsidian
  • lazygit
  • Vim 技巧
  • 分类
  • 标签
  • 归档
  • 网站
  • 资源
  • Vue 资源
GitHub (opens new window)
  • JavaScript

  • TypeScript

  • Vue

  • ElementUI

    • 基于 ElementUI 封装的 TextEllipsis
    • 基于 ElementUI 封装的 Tree
      • 展开 level 级
        • props
        • watch
        • methods
        • 延伸
        • 代码
        • 存在的问题
      • 全选
        • props
        • 结构样式
        • methods
        • 存在的问题
      • Attributes/Events/slot/方法
        • \$attrs
        • \$listeners
        • slot
        • 方法
    • 基于 ElementUI 封装的 Tree2
    • 基于 ElementUI 封装的 SelectTree
    • 基于 ElementUI 封装的 NumberInput
    • 基于 ElementUI 封装的基础 table 和 form
    • 基于 ElementUI 封装的 TreeTable
    • 基于 VuePress 搭建一个类似 ElementUI 的说明文档
    • el-tree 节点过滤加载对应子节点
    • 简单调试 node_modules 源码
    • ElementUI 问题集合
    • 树形表格更新后保持折叠状态
    • 开始和完成时间互相限制
  • React

  • AntD

  • 前端
  • ElementUI
Henry
2020-01-02
目录

基于 ElementUI 封装的 Tree

继上一篇 基于 ElementUI 封装的 SelectTree | Henry 后, 发现 tree 也需要封装一下, 增加一个全选功能和展开到 level 级的功能

源码 (opens new window)在这里

# 展开 level 级

思路: 先找出 tree 的所有 node, 再根据传入的 level 来判断 node 是展开还是折叠状态

所以我们需要接收一个 level 的 props

# props

props: {
  level: {
    type: Number,
    default: 1
  }
}
1
2
3
4
5
6

# watch

如果要动态改变展开的层级, 那必须 watch level

watch: {
  level: {
    handler: 'expandToLevel',
    immediate: true
  }
}
1
2
3
4
5
6

# methods

在这里我们可以获取所有 node: vue.js - element-ui 的 tree 树形组件怎么控制全部展开和全部折叠啊? - SegmentFault 思否 (opens new window)

const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
console.log('TCL: expandToLevel -> allNodes', allNodes)
1
2
3

在 node 中发现 expanded 和 level 这两个属性, 那么比较 node 和 props 的 level , 然后根据结果修改 expanded 这个属性的值应该就可以控制展开折叠了吧

/**
 * @method 展开至指定层级
 * @param {Number} level 要展开至几级?0, 1, 2, 3...
 **/
expandToLevel(level) {
  this.$nextTick(() => {
    const elTreeStore = this.$refs[this.ref].store
    const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
    console.log('TCL: expandToLevel -> allNodes', allNodes)
    if (level === 0) {
      // 展开全部
      allNodes.forEach(node => {
        node.expanded = true
      })
    } else {
      allNodes.forEach(node => {
        if (node.level >= level) {
          node.expanded = false
        } else {
          node.expanded = true
        }
      })
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

完全符合预期的效果

# 延伸

在 node 的 __proto__ 中发现两个方法: expand 和 collapse, 这不就是展开和折叠吗?

proto

查看一下源码:

Node.prototype.expand = function expand(callback, expandParent) {
  var _this = this;

  var done = function done() {
    if (expandParent) {
      var parent = _this.parent;
      while (parent.level > 0) {
        parent.expanded = true;
        parent = parent.parent;
      }
    }
    _this.expanded = true;
    if (callback) callback();
  };

  if (this.shouldLoadData()) {
    this.loadData(function (data) {
      if (data instanceof Array) {
        if (_this.checked) {
          _this.setChecked(true, true);
        } else if (!_this.store.checkStrictly) {
          reInitChecked(_this);
        }
        done();
      }
    });
  } else {
    done();
  }
};

Node.prototype.collapse = function collapse() {
  this.expanded = false;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

看起来 collapse 做的事情和我们是一样的, 那就还是用我们自己的吧

expand 除了将当前节点和全部父节点展开以外, 貌似还和懒加载有关, 难道我们的 node.expanded = true 无法对懒加载的数据进行展开折叠吗?

那我们就拿官方的示例来试一下吧

<tree :props="props" :load="loadNode" :level="level" lazy show-checkbox></tree>
1
level: 1,
props: {
  label: 'name',
  children: 'zones',
  isLeaf: 'leaf'
}

loadNode(node, resolve) {
  console.log('TCL: loadNode -> node, resolve', node, resolve)
  if (node.level === 0) {
    return resolve([{ name: 'region' }])
  }
  if (node.level > 1) return resolve([])

  setTimeout(() => {
    const data = [
      {
        name: 'leaf',
        leaf: true
      },
      {
        name: 'zone'
      }
    ]

    resolve(data)
  }, 500)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

果然无法展开折叠, 那我们使用 expand 方法呢?

expandToLevel(level) {
  this.$nextTick(() => {
    const elTreeStore = this.$refs[this.ref].store
    const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
    console.log('TCL: expandToLevel -> allNodes', allNodes)
    if (level === 0) {
      // 展开全部
      allNodes.forEach(node => {
        node.expand(null, true)
      })
    } else {
      allNodes.forEach(node => {
        if (node.level >= level) {
          node.collapse()
        } else {
          node.expand(null, false)
        }
      })
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

发现也不行

原来是_getAllNodes 获取的是空数组, 那 forEach 里面的逻辑就不执行了呀

是因为没有默认的 data 吗? 加一个

<tree
  :data="[{ name: 'region11' }, { name: 'aa11' }]"
  :props="props"
  :load="loadNode"
  :level="level"
  lazy
  show-checkbox
></tree>
1
2
3
4
5
6
7
8

发现还是空数组.

这就难办了呀, 无法获取全部数据, 那怎么控制展开折叠呀

目前本人没有找到有效的方法获取懒加载的所有数据, 话说好像也不好获取吧, 应该说没法获取吧. 懒加载的数据都是在点击节点后才加载的, 初始化应该是拿不到的

那这样看来, expand 方法目前和咱们的结果是一样的, 而且 expand 还将父节点也展开了, 而咱们在父节点上也调用了这个方法, 明显就是重了.

那关于展开的方式现在有三种:

  1. 直接使用 node.expanded = true, 方便省事
  2. 使用 expand 方法, 但在每一个 node 上使用会有效率问题, 重复给父节点的 expanded 赋值, 当然也不会有太大的影响
  3. 如果是展开全部, 使用 expand 方法, 否则使用 node.expanded = true

关于 方式3 的说明: expand 存在效率问题, 但可以根据 isLeaf 找出每一个树的最末节点的, 只要在这个节点调用方法即可; 如果是展开到某一级, 如果使用 expand, 那么必须根据 level 找出每一个树相对于 level 的末节点(比如 tree 有三个根节点, 第一个根节点有三级, 另两个有两级, 那么当 level = 3 时, 第一个的末节点就是 3, 而另外两个就是 2), 这样才不会重复赋值, 但找这个末节点有点麻烦, 还不如直接给符合展开条件的节点直接赋值快呢

# 代码

expandToLevel(level) {
  this.$nextTick(() => {
    const elTreeStore = this.$refs[this.ref].store
    const allNodes = elTreeStore._getAllNodes().sort((a, b) => b.level - a.level)
    if (level === 0) {
      // 展开全部
      allNodes.forEach(node => {
        node.isLeaf && node.expand(null, true)
      })
    } else {
      allNodes.forEach(node => {
        if (node.level >= level) {
          node.expanded = false
        } else {
          node.expanded = true
        }
      })
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 存在的问题

  1. 无法对懒加载进行展开折叠
  2. 如果用户不传 node-key, 默认值为空, _getAllNodes 获取到的是一个空数组, 后面逻辑都无法进行下去了, 那就只能是在 props 中给一个默认值了

# 全选

思路: 首先要在最顶层增加一个 checkbox, 要有 全选/半选/未选中 三种状态, 要和 tree 的 checkbox 相互联动

这个也需要参数来判断是否显示全选按钮

# props

props: {
  showCheckAll: {
    type: Boolean,
    default: false
  },
  showCheckbox: {
    type: Boolean,
    default: false
  }
}
1
2
3
4
5
6
7
8
9
10

# 结构样式

在最顶层增加一个 checkbox, 正好 element-ui 提供 indeterminate 状态 (opens new window)

<el-checkbox
  v-if="showCheckAll && showCheckbox"
  class="b-tree-check-all"
  :indeterminate="isIndeterminate"
  v-model="checkAll"
  @change="handleCheckAllChange"
  >全选</el-checkbox
>
1
2
3
4
5
6
7
8
.b-tree-check-all {
  padding-left: 8px;
  font-weight: normal;
  .el-checkbox__label {
    color: #606266;
  }
}
1
2
3
4
5
6
7

# methods

首先处理全选按钮的逻辑:

  1. isIndeterminate 的值只能是 false
  2. emit check 事件
  3. 根据是否全选来判断是否需要获取所有 node, 使用 setCheckedKeys 对 tree 进行选中处理

关于只 emit check 事件而不同时 emit check-change 的说明: check-change 受 render-after-expand 的影响, 如果 render-after-expand = true, 那么第一次如果没有展开, 点击父节点只拿到父节点的 check-change 事件, 如果展开以后, 点击父节点, 就会拿到父节点和子节点的 check-change 事件. 目前好像没有业务需要这样的, 感觉 check 的数据就足以, 如果以后需要再加上

// 处理全选
handleCheckAllChange() {
  this.isIndeterminate = false
  let checkedKeys = []
  if (this.checkAll) {
    const elTreeStore = this.$refs[this.ref].store
    const checkedNodes = elTreeStore._getAllNodes()
    checkedKeys = checkedNodes.map(({ key }) => key)
    this.$emit(
      'check',
      { [this.$refs[this.ref].props.label || 'label']: '全选' },
      {
        checkedNodes,
        checkedKeys,
        halfCheckedNodes: [],
        halfCheckedKeys: []
      }
    )
  } else {
    this.$emit(
      'check',
      { [this.$refs[this.ref].props.label || 'label']: '全选' },
      {
        checkedNodes: [],
        checkedKeys: [],
        halfCheckedNodes: [],
        halfCheckedKeys: []
      }
    )
  }
  this.$refs[this.ref].setCheckedKeys(checkedKeys)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

再处理 tree 的选中逻辑:

  1. 首先判断 showCheckAll 和 showCheckbox, 如果为 false, 直接 return
  2. 根据 checkedKeys.length 和 allNodes.length 比较来给 checkAll 和 isIndeterminate 赋值
// el-tree 复选框被点击
handleCheck(data, checked) {
  if (!this.showCheckAll || !this.showCheckbox) {
    return
  }
  const { checkedKeys } = checked
  const elTreeStore = this.$refs[this.ref].store
  const allNodes = elTreeStore._getAllNodes()
  if (checkedKeys.length) {
    if (checkedKeys.length === allNodes.length) {
      this.checkAll = true
      this.isIndeterminate = false
    } else {
      this.checkAll = false
      this.isIndeterminate = true
    }
  } else {
    this.isIndeterminate = false
    this.checkAll = false
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 存在的问题

一: element-ui 的 filter, 只是将数据在页面隐藏, 使用 _getAllNodes 仍然能获取到, 所以还需要过滤一下

const elTreeStore = this.$refs[this.ref].store
const allNodes = elTreeStore._getAllNodes().filter(node => node.visible)
1
2

虽然咱们自己的全选使用过滤实现了页面与实际数据相同, 但 element-ui 自己仍然有 bug, 具体请看这里: [Bug Report] with tree components, multi box filter, select the parent node, can only choose to filter out of the current node? · Issue #18522 · ElemeFE/element (opens new window)

二: 只有点击复选框的时候, 全选 复选框可以联动, 通过 setCheckedNodes 、 setCheckedKeys 和 default-checked-keys 设置目前勾选的节点时, 无法联动, 具体解决方案请查看 基于 ElementUI 封装的 Tree2 | Henry

# Attributes/Events/slot/方法

# $attrs

如果看过本人封装 SelectTree 的那篇文章的话, 大家应该知道 v-bind=obj 可以传入一个对象的所有属性, 但最近浏览 Vue 官网发现 $attrs:

包含了父作用域中不作为 prop 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

# $listeners

还有 $listeners:

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。

那么 el-tree 进行封装后:

<el-tree
  :ref="ref"
  v-bind="$attrs"
  :show-checkbox="showCheckbox"
  v-on="$listeners"
  @check="handleCheck"
>
</el-tree>
1
2
3
4
5
6
7
8

由于 show-checkbox 已经在 props 中声明了, 那么这里还需要写一下

# slot

由于 el-tree 支持 scoped slot 的自定义节点内容, 那咱们也得要实现一下这个

首先咱们需要一个标签来拿到 el-tree 的两个参数 node 和 data, 而且还需要一个 slot 标签来将传入咱们封装组件的 scoped slot 接收, 并将拿到的两个参数 node 和 data 传递出去.

阅读 插槽 — Vue.js (opens new window) 后发现可以合并到一个 slot 标签上:

<slot slot-scope="{ node, data }" v-bind="{ node, data }"> {{ node.label }} </slot>
1

slot-scope 接收 node 和 data, v-bind 传递出去, node.label 展示默认值

# 方法

本人以为有 $attrs 和 $listeners, 那方法是不是也有这么一个属性呢? 但没有在 Vue 官网发现这个参数, 如果有大佬找到, 望不吝赐教.

而且也在微信群和网上问过大佬: javascript - vue 封装组件时候如何向上传递方法? - SegmentFault 思否 (opens new window), 大部分人都是说 $refs.$refs 这么一直通过 refs 找到 el-tree, 但总感觉这种方式不太优雅; 还有大佬说找到 el-tree 后可以缓存起来, 但封装 SelectTree 的时候发现缓存会有问题, 而且本质还是上面那种方法, 所以还是不想使用

而我本人想到的最笨的办法是将 el-tree 的所有方法在封装组件中挨个写一遍, 类似下面这种:

getCheckedNodes(leafOnly, includeHalfChecked) {
  return this.$refs[this.ref].getCheckedNodes(leafOnly, includeHalfChecked);
}
1
2
3

首先不说有多麻烦, 万一以后 el-tree 又增加或删除方法, 我还得参考官网维护, 这个成本就有点大了

终于在 vue.js - 如何获取一个 vue 实例里的所有方法 - SegmentFault 思否 (opens new window) 这个问答中找到了一种方式, 成功解决了问题, 虽然本人认为不是最优雅的解法, 但对比上面的还是好很多了.

以下再粘贴一下本人的回答吧

就以  el-tree  为例来说吧, 以下均为简略代码

Tree.vue

<el-tree ref="elTree"></el-tree>
1
import { Tree } from 'element-ui'

export default {
  name: 'Tree',
  components: { [Tree.name]: Tree },
  methods: {
    // 向上传递 el-tree 的方法
    ...Tree.methods
  }
}
1
2
3
4
5
6
7
8
9
10

MyTree.vue

<tree ref="tree" v-bind="treeProps"></tree>
1
import Tree from 'plugins/Tree'

getCheckedNodes() {
  console.log('this.$refs.tree', this.$refs.tree.getCheckedNodes)
}
1
2
3
4
5

输出:

this.$refs.tree ƒ getCheckedNodes(leafOnly, includeHalfChecked) {
  return this.store.getCheckedNodes(leafOnly, includeHalfChecked);
}
1
2
3

this.store 的 this  是  MyTree  这个实例, 就没有  store  这个属性, 这个属性是在  el-tree  上的.

所以这种方式是不行的, 因为  this  指向不同, 而我也没找到好办法可以在这种绑定  this, 如果有大佬知道, 望不吝赐教

那就只能是在  mounted  上来绑定  this  了

mounted() {
  for (let key in this.$refs.elTree) {
    if (!(key in this) && typeof this.$refs.elTree[key] === 'function') {
      this[key] = this.$refs.elTree[key].bind(this.$refs.elTree)
      console.log('TCL: mounted -> key', key)
    }
  }
}
1
2
3
4
5
6
7
8

输出  key  看了一下, el-tree  官方文档上的方法都有, 好像还多了两个, 不过不影响使用了, 而且这样也不用再单独引入  Tree  了

至此, 也算 "完美" 解决问题了, 但还是感觉不是太优雅. 比起  $refs.$refs.$refs  来说还是这种好一点

编辑 (opens new window)
#Js#ElementUI
上次更新: 5/27/2023, 1:02:05 PM
基于 ElementUI 封装的 TextEllipsis
基于 ElementUI 封装的 Tree2

← 基于 ElementUI 封装的 TextEllipsis 基于 ElementUI 封装的 Tree2→

最近更新
01
version 1.15
07-01
02
version 1.14
06-27
03
version 1.13
06-27
更多文章>
Theme by Vdoing | Copyright © 2017-2023 HenryTSZ | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式