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
    • 基于 ElementUI 封装的 Tree2
    • 基于 ElementUI 封装的 SelectTree
      • 分析大佬源码
      • 初稿
      • 优化 Attributes
      • 处理逻辑
      • 完成(其实是一稿, 大家都懂得)
      • 修正
      • 实际工作中发现的问题
        • tree 为 undefined(已解决)
        • EditableElements 使用 select-tree 时, placeholder 不会自动生成(已解决)
        • 单选时只能选择叶子节点, 但会选择父级(已解决)(改为 Tree 解决)
        • 单选时只能选择叶子节点, 但点击父节点后, 父节点会有选中状态(已解决)
        • v-model 绑定的值赋值为空后, 界面仍显示上次的值(已解决)
        • 在 el-form 必填验证时, 第一次选择仍提示为必填(已解决)
        • 在 el-form 必填验证时, 选择一项后再调用 form 重置功能仍提示为必填(已解决)
        • 在 el-form 必填验证时, 多选初始化就提示必填
    • 基于 ElementUI 封装的 NumberInput
    • 基于 ElementUI 封装的基础 table 和 form
    • 基于 ElementUI 封装的 TreeTable
    • 基于 VuePress 搭建一个类似 ElementUI 的说明文档
    • el-tree 节点过滤加载对应子节点
    • 简单调试 node_modules 源码
    • ElementUI 问题集合
    • 树形表格更新后保持折叠状态
    • 开始和完成时间互相限制
  • React

  • AntD

  • 前端
  • ElementUI
Henry
2019-11-29
目录

基于 ElementUI 封装的 SelectTree

最近重构项目, 遇到一个需要 SelectTree 的组件, 就在网上找了一圈, 发现 Element-UI 二次封装实现 TreeSelect 树形下拉选择组件 - sleepwalker_1992 的专栏 - CSDN 博客 (opens new window)这个还不错, 但奈何作者不更新了, 而且现在样式有点问题, 就想着参考大佬的思路自己也封装一下

话不多说, 先将本人源码 (opens new window)奉上. 好了, 开干!

# 分析大佬源码

<template>
  <div>
    <div class="mask" v-show="isShowSelect" @click="isShowSelect = !isShowSelect"></div>
    <el-popover
      placement="bottom-start"
      trigger="manual"
      v-model="isShowSelect"
      @hide="popoverHide"
    >
      <el-tree
        ref="tree"
        :data="data"
        :props="defaultProps"
        :show-checkbox="multiple"
        @node-click="handleNodeClick"
        @check-change="handleCheckChange"
      ></el-tree>
      <el-select
        slot="reference"
        ref="select"
        v-model="selectedData"
        :multiple="multiple"
        @click.native="isShowSelect = !isShowSelect"
        @remove-tag="removeSelectedNodes"
        @clear="removeSelectedNode"
        @change="changeSelectedNodes"
      >
        <el-option
          v-for="item in options"
          :key="item.value"
          :label="item.label"
          :value="item.value"
        ></el-option>
      </el-select>
    </el-popover>
  </div>
</template>
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
35
36
37

首先大佬利用 el-popover 弹出框为基础, 将 el-select 作为 reference 的 slot 触发 Popover 显示的 HTML 元素, el-tree 作为内容. 并且利用 select 的 option 来解决 value 和 label 的转化(虽然自己也可以解决, 但既然组件已经有轮子了, 那就拿过来用即可)

一开始我也感觉应该是这样的, 查看重构前的代码发现了另一种思路:

<el-select ref="select" v-model="selectData">
  <el-option value=""></el-option>
  <tree
    :data="data"
    :isAllOpen="false"
    :isShowCheck="true"
    :isMultiple="true"
    :isAllChecked="false"
  ></tree>
</el-select>
1
2
3
4
5
6
7
8
9
10
.el-select-dropdown__item {
  display: none;
}
1
2
3

给 el-option 一个默认值, 并利用 css 将其隐藏, 这样下方的 tree 组件就相当于 el-option 了

那咱们就把两者结合一下

# 初稿

<template>
  <div class="select-tree">
    <el-select
      v-model="selectData"
      :multiple="multiple"
      :disabled="disabled"
      :value-key="valueKey"
      :size="size"
      :clearable="clearable"
      :collapse-tags="collapseTags"
      :multiple-limit="multipleLimit"
      :placeholder="placeholder"
      @change="selectChange"
      @remove-tag="removeTag"
    >
      <el-option
        v-for="item in selectOptions"
        :key="item.value"
        :value="item.value"
        :label="item.label"
      ></el-option>
      <el-tree
        ref="tree"
        :data="data"
        :node-key="nodeKey"
        :props="props"
        :highlight-current="highlightCurrent"
        :default-expand-all="defaultExpandAll"
        :expand-on-click-node="expandOnClickNode"
        :check-on-click-node="checkOnClickNode"
        :auto-expand-parent="autoExpandParent"
        :default-expanded-keys="defaultExpandedKeys"
        :show-checkbox="multiple"
        :check-strictly="checkStrictly"
        :default-checked-keys="defaultCheckedKeys"
        :current-node-key="currentNodeKey"
        :accordion="accordion"
        :indent="indent"
        @node-click="handleNodeClick"
        @check-change="handleCheckChange"
      ></el-tree>
    </el-select>
  </div>
</template>
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
35
36
37
38
39
40
41
42
43
44

咱封装组件一般都喜欢保留原组件的 Attributes, 这样别人使用的时候按照原来的习惯使用即可. 但这样就有问题了: 首先是在组件上添加了太多的 Attributes, props 也需要都指定一下, 有时候还需要指定默认值, 工作量太大了, 而且用户实际上只使用几个属性. 理想情况下应该是: 咱们不对用户传入的属性做处理, 直接仍给原组件去处理

查看 Vue 官方文档后发现可以传入一个对象的所有属性 (opens new window), 那这样我不是接收两个属性就可以了吗? 一个是 select 的, 一个是 tree 的

# 优化 Attributes

<template>
  <div class="select-tree">
    <el-select
      ref="select"
      v-model="selectData"
      :clearable="false"
      v-bind="selectProps"
      @visible-change="handleVisibleChange"
      @remove-tag="handleRemoveTag"
      @clear="handleClear"
    >
      <el-option
        v-for="item in selectOptions"
        :key="item.value"
        :value="item.value"
        :label="item.label"
      ></el-option>
      <el-tree
        :key="treeKey"
        ref="tree"
        v-bind="treeBind"
        @node-click="handleNodeClick"
        @current-change="handleCurrentChange"
        @check-change="handleCheckChange"
      ></el-tree>
    </el-select>
  </div>
</template>
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
export default {
  name: 'SelectTree',
  props: {
    value: {
      type: [String, Number, Array],
      required: true
    },
    selectProps: {
      type: Object,
      default() {
        return {}
      },
      required: true
    },
    treeProps: {
      type: Object,
      default() {
        return {}
      },
      required: true
    },
    // 单选时是否只能选择叶子节点
    currentIsLeaf: {
      type: Boolean,
      default: false
    }
  },
  data() {
    return {
      treeKey: Math.random(),
      multiple: false,
      selectData: '',
      selectOptions: []
    }
  },
  computed: {
    treeBind() {
      return {
        showCheckbox: this.selectProps.multiple,
        highlightCurrent: !this.selectProps.multiple,
        expandOnClickNode: this.expandOnClickNode,
        ...this.treeProps,
        defaultCheckedKeys: this.selectProps.multiple ? this.value : [],
        currentNodeKey: this.selectProps.multiple ? '' : this.value
      }
    },
    expandOnClickNode() {
      return this.multiple ? true : this.currentIsLeaf ? true : 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

传入的参数:

selectProps: {
  multiple: true,
  // 'collapse-tags': true,
  collapseTags: true,
  clearable: true
},
treeProps: {
  data,
  showCheckbox: false,
  // expandOnClickNode: false,
  props: { children: 'childrenList', label: 'menuName' },
  nodeKey: 'menuId'
}
1
2
3
4
5
6
7
8
9
10
11
12
13

使用 v-bind 是不是一下就少了好多代码, 而且也不用自己处理属性了, 完全把用户传过来的直接扔给了组件

细心观察的同学会发现上面的代码其实有对比参照的.

先看 select 的属性:

:clearable="false"
v-bind="selectProps"
1
2

本来是想定义一些默认属性, 然后用户的属性可以覆盖

我以为属性这样写, 会和对象一样, 下面的属性覆盖上面的, 却发现无法覆盖, 而且无论 :clearable 放在 v-bind 上面还是下面, 最终都是 false, 貌似 v-bind 中的 clearable 没有生效

那就只能先处理一下, 再使用 v-bind 绑定. 最后成果就是 tree 中的 v-bind="treeBind".

但这样也有一个问题: 那就是用户只能传入驼峰值, 不能传入中划线分割的值, 否则无法覆盖默认值(注: 比较过两种传值, Vue 推荐使用中划线方式, 但现在用户其实传入的是对象, 中划线方式还需要外层包围引号, 而且对象中写中划线感觉很别扭, 就定为驼峰了)

注: 如果看过本人对 tree 的封装文章的同学, 这里可能会问为什么不用 v-bind="$attrs", 一是本人封装 tree 的时候才算了解这个属性; 二是这里有两个组件, 虽然目前它们需要的属性名都不一样, 但不知道以后会不会有同名但作用不同的属性, 而且传一堆没用的属性过去也不好, 所以就不改了

# 处理逻辑

主要考虑的逻辑就是 tree 的单选和多选

@current-change="handleCurrentChange"
@check-change="handleCheckChange"

// 单选,节点被点击时的回调,返回被点击的节点数据
handleCurrentChange() {
  // 如果是多选就返回
  if (this.multiple) return
  // 获取当前选中的数据. 注: 是 data, 而不是 node
  const currentNode = this.$refs.tree.getCurrentNode()
  // 这个才是 node
  const node = this.$refs.tree.getNode(currentNode)
  // 判断是否只能选择叶子节点
  if (this.currentIsLeaf && !node.isLeaf) return
  this.selectOptions = []
  this.selectData = ''
  const value = node.key
  const label = node.label
  this.selectOptions.push({
    value,
    label
  })
  this.selectData = value
  this.$refs.select.blur()
},
// 多选,节点勾选状态发生变化时的回调
handleCheckChange() {
  // 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
  this.selectOptions = [{}]
  this.selectData = []
  // 获取当前选中的数据. 注: 是 data, 而不是 node
  const checkedNodes = this.$refs.tree.getCheckedNodes()
  checkedNodes.forEach(node => {
    const value = node[this.$refs.tree.nodeKey]
    this.selectOptions.push({
      value,
      label: node[this.$refs.tree.props.label]
    })
    this.selectData.push(value)
  })
}
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
35
36
37
38
39
40

具体逻辑上面注释已经很清楚了, 就不多做说明了

这里先吐槽一下, 明明是 getCurrentNode 和 getCheckedNode, 返回的却是 data. 这个应该是历史遗留问题吧, 就不深究了. 幸好官方提供了 getNode 这个方法

接下来说正事. 想说的是在取数据方面又是一个对比: 单选步骤是: 先拿到当前 data, 再通过 data 拿到当前 node, 不论你传入的 nodeKey 和 props.label 是什么, node 都会转化为固定的 key 和 label; 多选的步骤是拿到当前 data, 然后直接通过 this.$refs.tree 获取 nodeKey 和 props.label, 再通过 data 取值

这里说一个小插曲, 本人看到 this.$refs.tree 使用很多次, 就想着能不能在初始化的时候提取出来, 这样以后就可以直接用了. 考虑过 tree 的 set 方法是否能生效, 测试了一下发现可以成功, 最后发现还是太年轻, 由于数据刷新, 使用初始化提取出来的仍然是旧值, 没有实时更新, 所以还是老老实实用 this.$refs.tree 吧

# 完成(其实是一稿, 大家都懂得)

<template>
  <div class="select-tree">
    <el-select
      ref="select"
      v-model="selectData"
      v-bind="selectProps"
      @visible-change="handleVisibleChange"
      @remove-tag="handleRemoveTag"
      @clear="handleClear"
    >
      <el-option
        v-for="item in selectOptions"
        :key="item.value"
        :value="item.value"
        :label="item.label"
      ></el-option>
      <el-tree
        :key="treeKey"
        ref="tree"
        v-bind="treeBind"
        @current-change="handleCurrentChange"
        @check-change="handleCheckChange"
      ></el-tree>
    </el-select>
  </div>
</template>

<script>
  export default {
    name: 'SelectTree',
    props: {
      value: {
        type: [String, Number, Array],
        required: true
      },
      selectProps: {
        type: Object,
        default() {
          return {}
        },
        required: true
      },
      treeProps: {
        type: Object,
        default() {
          return {}
        },
        required: true
      },
      // 单选时是否只能选择叶子节点
      currentIsLeaf: {
        type: Boolean,
        default: false
      }
    },
    data() {
      return {
        treeKey: Math.random(),
        selectData: '',
        selectOptions: []
      }
    },
    computed: {
      treeBind() {
        return {
          showCheckbox: this.selectProps.multiple,
          highlightCurrent: !this.selectProps.multiple,
          expandOnClickNode: this.expandOnClickNode,
          ...this.treeProps,
          defaultCheckedKeys: this.selectProps.multiple ? this.value : [],
          currentNodeKey: this.selectProps.multiple ? '' : this.value
        }
      },
      multiple() {
        return this.selectProps.multiple
      },
      expandOnClickNode() {
        return this.multiple ? true : this.currentIsLeaf ? true : false
      }
    },
    watch: {
      value() {
        // 为了检测 v-model 的变化
        if (this.value + '' !== this.selectData + '') {
          this.treeKey = Math.random()
          this.init()
        }
      }
    },
    methods: {
      init() {
        this.$nextTick(() => {
          if (this.multiple) {
            this.handleCheckChange()
          } else {
            this.handleCurrentChange()
          }
        })
      },
      // select 下拉框出现/隐藏
      handleVisibleChange(val) {
        // 下拉框隐藏并且值改变后
        if (!val && this.value + '' !== this.selectData + '') {
          this.$emit('input', this.selectData)
          this.$emit('change', this.selectData)
        }
        this.$emit('visible-change', val)
      },
      // select 清空
      handleClear() {
        if (this.$refs.tree.showCheckbox) {
          this.selectData = []
          this.$refs.tree.setCheckedKeys([])
        } else {
          this.selectData = ''
          this.$refs.tree.setCurrentKey(null)
        }
        this.$emit('input', this.selectData)
        this.$emit('change', this.selectData)
        this.$emit('clear')
      },
      // select 移除 tag
      handleRemoveTag(val) {
        this.$refs.tree.setChecked(val, false)
        let node = this.$refs.tree.getNode(val)
        if (!this.$refs.tree.checkStrictly && node.childNodes.length > 0) {
          this.tree2List(node).map(item => {
            if (item.childNodes.length <= 0) {
              this.$refs.tree.setChecked(item, false)
            }
          })
          this.handleCheckChange()
        }
        this.$emit('input', this.selectData)
        this.$emit('change', this.selectData)
        this.$emit('remove-tag', val)
      },
      // 单选, 节点被点击时的回调, 返回被点击的节点数据
      handleCurrentChange() {
        // 如果多选, 不处理
        if (this.multiple) return
        // 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
        this.selectOptions = [{}]
        const currentNode = this.$refs.tree.getCurrentNode()
        // 初始值为空
        if (!currentNode) return
        const node = this.$refs.tree.getNode(currentNode)
        // 判断叶子节点
        if (this.currentIsLeaf && !node.isLeaf) return
        this.selectOptions = []
        this.selectData = ''
        const value = node.key
        const label = node.label
        this.selectOptions.push({
          value,
          label
        })
        this.selectData = value
        this.$refs.select.blur()
      },
      // 多选,节点勾选状态发生变化时的回调
      handleCheckChange() {
        // 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
        this.selectOptions = [{}]
        this.selectData = []
        const checkedNodes = this.$refs.tree.getCheckedNodes()
        checkedNodes.forEach(node => {
          const value = node[this.$refs.tree.nodeKey]
          this.selectOptions.push({
            value,
            label: node[this.$refs.tree.props.label]
          })
          this.selectData.push(value)
        })
      },
      tree2List(tree) {
        let queen = []
        let out = []
        queen = queen.concat(tree)
        while (queen.length) {
          let first = queen.shift()
          if (first.childNodes) {
            queen = queen.concat(first.childNodes)
          }
          out.push(first)
        }
        return out
      }
    },
    mounted() {
      this.init()
    }
  }
</script>
<style lang="less" scoped>
  .select-tree {
    display: inline-block;
    width: 100%;
  }
  .el-select-dropdown__item {
    display: none;
  }
</style>
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203

# 修正

经后期测试, 通过 this.$refs.tree 获取 nodeKey 和 props.label 存在一定问题, 如果用户未传入 nodeKey 或 props.label, 那么拿到的值都是 undefined, 这种方式还是不靠谱, 还是用单选方式靠谱. 当时本人测试的时候这两个值都传入了, 所以没有测出 bug, 实际使用的时候, 没有传入 nodeKey, 导致获取到的值都是 undefined

so, 咱们改成单选方式试试:

// 多选,节点勾选状态发生变化时的回调
handleCheckChange() {
  // 给 selectOptions 一个默认值, 防止出现无数据, 从而无法显示 tree
  this.selectOptions = [{}]
  this.selectData = []
  const checkedNodes = this.$refs.tree.getCheckedNodes()
  checkedNodes.forEach(node => {
    const checkedNode = this.$refs.tree.getNode(node)
    const value = checkedNode.key
    this.selectOptions.push({
      value,
      label: checkedNode.label
    })
    this.selectData.push(value)
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

使用以上代码发现 getNode 永远返回 null, 原因是 nodeKey 为 undefined, 无法获取 node. 而且 getCheckedNodes 可以正常返回数据, getCheckedKeys 返回的都是 undefined. 所以必须给 nodeKey 一个默认值了.

treeBind() {
  return {
    showCheckbox: this.selectProps.multiple,
    highlightCurrent: !this.selectProps.multiple,
    expandOnClickNode: this.expandOnClickNode,
    nodeKey: 'id',
    ...this.treeProps,
    defaultCheckedKeys: this.selectProps.multiple ? this.value : [],
    currentNodeKey: this.selectProps.multiple ? '' : this.value
  }
}
1
2
3
4
5
6
7
8
9
10
11

这样, getCheckedNodes, getCheckedKeys, getNode 都可以正常使用了

# 实际工作中发现的问题

# tree 为 undefined(已解决)

由于本人后面封装了 tree, 就想着使用 tree 代替这里的 el-tree, 并且通过懒加载方式引入组件

components: {
  Tree: resolve => require(['plugins/Tree'], resolve)
}
1
2
3

结果杯具了, this.$refs.tree 初始化永远是 undefined, 只有手动点击下拉框后才能正常获取.

一开始以为加一个 $nextTick 就好了, 最后发现没用. 然后就想到是不是懒加载影响的, 替换成 import Tree from 'plugins/Tree' 一下就好了. 看来什么东西也不能太绝对了, 太追求性能也不行

# EditableElements 使用 select-tree 时, placeholder 不会自动生成(已解决)

如果不知道 EditableElements, 可以看看这篇文章: 基于 ElementUI 封装的基础 table 和 form | Henry

原因是现在只能通过 selectProps 来传入 placeholder, 直接传入的话是不认的

其实这里设计的有点问题, tree 的 props 必须通过 treeProps 传入, select 的 props 应该是直接传入即可, 就和 el-select 一样

那就兼容一下吧:

<el-select
  v-bind="{ ...$attrs, ...selectProps }"
>
1
2
3
props: {
  multiple: {
    type: Boolean,
    default: false
  }
},
computed: {
  treeBind() {
    return {
      showCheckbox: this.isMultiple,
      highlightCurrent: !this.isMultiple,
      expandOnClickNode: this.expandOnClickNode,
      nodeKey: 'id',
      ...this.treeProps,
      defaultCheckedKeys: this.isMultiple ? this.value : [],
      currentNodeKey: this.isMultiple ? '' : this.value
    }
  },
  isMultiple() {
    return this.selectProps.multiple || this.multiple
  },
  expandOnClickNode() {
    return this.isMultiple ? true : this.currentIsLeaf
  }
}
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

this.multiple 修改为 this.isMultiple 即可

这样就可以

由于本组件已经在项目中使用了, 去掉 selectProps 改动有点大, 只能这样兼容一下

如果是新项目, 可以去掉 selectProps 了:

<el-select
  v-bind="$attrs"
>
1
2
3

# 单选时只能选择叶子节点, 但会选择父级(已解决)(改为 Tree 解决)

其实这个应该是 Tree 的功能, 当初未想到这些, 导致加到了这里. 但思路是相通的

基于 ElementUI 封装的 Tree2 | Henry

目前只是通过 node.isLeaf 来判断是否是叶子节点, 但有时候只有一个父级, 那么这个父级既是父节点, 又是叶子节点, 我们有时候是不能选择这个节点的. 当然如果你们的需求就是可以选择这个节点, 那就不用往下看了, 目前完全可以胜任你们的需求

而且判断是不是叶子节点, 不同的数据源会有不同的判断方式, 所以需要放出一个方法, 让使用者自己判断是否是叶子节点

添加一个 props:

props: {
  /**
    * @description: 自定义单选时只能选择子节点方法; 优先级高于 currentIsLeaf
    * @param {data: Object}: 当前节点数据
    * @param {node: Object}: 当前节点 Node 对象
    * @return: Boolean
    */
  isLeafFun: {
    type: Function
  }
},
methods: {
  // 单选, 节点被点击时的回调, 返回被点击的节点数据
  handleCurrentChange() {
    // do something
    // 判断叶子节点
    if (this.isLeafFun ? this.isLeafFun(currentNode, node) : !node.isLeaf && this.currentIsLeaf) {
      return
    }
    // do something
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 单选时只能选择叶子节点, 但点击父节点后, 父节点会有选中状态(已解决)

比如当前选中一个叶子节点, 但需要选择另一个叶子节点, 当点击这个叶子节点的父节点时, 初始选中的叶子节点的选中状态消失了, 出现在父节点上了; 虽然不会改变选择框中的值, 但也算一个显示 bug

select bug

所以我们需要在判断是否是叶子节点中做一些处理: 如果不是叶子节点, 需要设置当前选中为上一个已选中的叶子节点或 null(没有上一个已选中的叶子节点)

// 判断叶子节点
if (this.isLeafFun ? this.isLeafFun(currentNode, node) : !node.isLeaf && this.currentIsLeaf) {
  // 如果不是叶子节点, 设置当前选中节点仍为上一个叶子节点
  this.$refs.tree.setCurrentKey(this.selectData || null)
  return
}
1
2
3
4
5
6

# v-model 绑定的值赋值为空后, 界面仍显示上次的值(已解决)

主要问题出现在这里:

// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
  // do something
  const currentNode = this.$refs.tree.getCurrentNode()
  // 初始值为空
  if (!currentNode) return
  // do something
}
1
2
3
4
5
6
7
8

这里由于 v-model 传入的是一个空值, 在 tree 中无法找到(其实不只是空值, 只要是在 tree 中无法找到就有这个问题), 从而直接返回了, 并没有对 selectData 做清空操作, 导致仍显示上次的结果

所以我们需要在这里清空一下 selectData

// 单选, 节点被点击时的回调, 返回被点击的节点数据
handleCurrentChange() {
  // do something
  const currentNode = this.$refs.tree.getCurrentNode()
  // 当前传入的值在 tree 中无法找到, 需要清空 select 值
  if (!currentNode) {
    this.selectData = ''
    return
  }
  // do something
}
1
2
3
4
5
6
7
8
9
10
11

可能有小伙伴问了: 你下面也有清空操作, 这里也有, 那直接把清空操作提出来, 在这里清空不就好了吗? 下面就不用写了呀

但这里不能将清空操作提出来, 因为下面还有一个判断叶子节点的逻辑. 如果在这里清空了, 下面进入判断叶子节点的逻辑后, 直接返回了: 这里正常逻辑是维持界面显示不变, 但由于上面清空了, 导致与实际情况不符, 所以不能提出来

# 在 el-form 必填验证时, 第一次选择仍提示为必填(已解决)

required

如上图所示, 第一次选择后, 仍提示为必填, 只有第二次选择后才正常, 就好像每次验证的都是上次的结果

被这个问题困扰了好久, 最近终于知道为啥了

简略代码如下:

methods: {
  // 单选, 节点被点击时的回调, 返回被点击的节点数据
  handleCurrentChange() {
    // do something
    // 当前传入的值在 tree 中无法找到, 需要清空 select 值
    if (!currentNode) {
      this.selectData = ''
      this.selectNode = null
      return
    }
    // do something
    this.selectData = value
    this.selectNode = node.data
    console.log('blur')
    this.$refs.select.blur()
  },
  // select 下拉框出现/隐藏
  handleVisibleChange(val) {
    // 如果有过滤, 下拉框出现后, 重置搜索
    if (val && this.filterable) {
      this.filter()
    }
    // 下拉框隐藏并且值改变后
    if (!val && this.value + '' !== this.selectData + '') {
      this.$emit('input', this.selectData)
      this.$emit('change', this.selectData, this.selectNode)
      console.log('emit')
    }
    this.$emit('visible-change', val)
  }
}
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

输出:

console

由以上代码和图片可以看出, 当单选选完后, 调用 el-select 的 blur 方法, 此时其实已经触发验证了, 而我们 emit 此时才调用, 所以此时验证的是上一次的值, 所以第一次会提示必填

所以我们得在 blur 方法之前调用 emit 才可以

methods: {
  emitBase() {
    this.$emit('input', this.selectData)
    this.$emit('change', this.selectData, this.selectNode)
  },
  // 单选, 节点被点击时的回调, 返回被点击的节点数据
  handleCurrentChange() {
    // do something
    // 当前传入的值在 tree 中无法找到, 需要清空 select 值
    if (!currentNode) {
      this.selectData = ''
      this.selectNode = null
      this.emitBase()
      this.$refs.select.blur()
      return
    }
    // do something
    this.selectData = value
    this.selectNode = node.data
    this.emitBase()
    console.log('blur')
    this.$refs.select.blur()
  },
  // select 下拉框出现/隐藏
  handleVisibleChange(val) {
    // 如果有过滤, 下拉框出现后, 重置搜索
    if (val && this.filterable) {
      this.filter()
    }
    // 下拉框隐藏并且值改变后
    if (!val && this.value + '' !== this.selectData + '') {
      this.emitBase()
      console.log('emit')
    }
    console.log('visible', val)
    this.$emit('visible-change', val)
  }
}
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
35
36
37
38

结果:

required

输出:

console

由于在 blur 之前就 emit 了, 所以在 handleVisibleChange 中 this.value 和 this.selectData 是相同的, 所以没有输出 emit

但这样又出现新的问题: 初始化的时候就会调用 handleCurrentChange, 从而 emit change, 所以我们需要一个变量来标记一下是否是第一次

data: {
  isFirst: true
},
methods: {
  // 单选, 节点被点击时的回调, 返回被点击的节点数据
  handleCurrentChange() {
    // do something
    // 当前传入的值在 tree 中无法找到, 需要清空 select 值
    if (!currentNode) {
      this.selectData = ''
      this.selectNode = null
      if (this.isFirst) {
        this.isFirst = false
      } else {
        this.emitBase()
        this.$refs.select.blur()
      }
      return
    }
    // do something
    this.selectData = value
    this.selectNode = node.data
    if (this.isFirst) {
      this.isFirst = false
    } else {
      this.emitBase()
      console.log('blur')
      this.$refs.select.blur()
    }
  }
}
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

单选的问题解决了, 多选也需要解决一下:

multiple

但在多选的 handleCheckChange 中不能直接调用 emitBase, 因为会把 change 也 emit 出去, 但其实只有下拉框关闭(也就是 blur 后才能 emit change), 所以这里只 emit input

// 多选, 节点勾选状态发生变化时的回调
handleCheckChange() {
  // do something
  if (this.isFirst) {
    this.isFirst = false
  } else {
    this.$emit('input', this.selectData)
  }
}
1
2
3
4
5
6
7
8
9

但这里 emit input 后, 在 handleVisibleChange 中 this.value 和 this.selectData 是相同的, 这样就不会调用 emitBase 了, 无法 emit change了; 所以对于多选, 还需要一个变量: 在下拉框打开时保存 this.value, 在关闭后, 判断该变量与 this.value 是否相同

// select 下拉框出现/隐藏
handleVisibleChange(val) {
  // 下拉框出现并且是多选, 将 this.value 保存到变量 multipleTempValue
  if (val && this.multiple) {
    this.multipleTempValue = deepClone(this.value)
  }
  // 如果有过滤, 下拉框出现后, 重置搜索
  if (val && this.filterable) {
    this.filter()
  }
  // 下拉框隐藏并且是多选, 判断值是否变化
  if (!val && this.multiple && this.value + '' !== this.multipleTempValue + '') {
    this.$emit('change', this.selectData, this.selectNode)
  }
  this.$emit('visible-change', val)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 在 el-form 必填验证时, 选择一项后再调用 form 重置功能仍提示为必填(已解决)

required

经过简单调试后, 发现是 $nextTick 的原因, 但又必须要用这个方法, 因为使用 key 重新加载 Tree 后, 只有在 $nextTick 后 Tree 才加载完成, 才可以使用 Tree 的方法

那有没有不使用 $nextTick 也能知道 Tree 加载完成, 并可以使用 Tree 的方法呢?

答案是肯定的, 那就是 @hook: 外部监听生命周期函数

我们在 Tree 中监听 @hook:mounted 并执行 init 就可以了吧

<tree
  :key="treeKey"
  ref="tree"
  v-bind="treeBind"
  @current-change="handleCurrentChange"
  @check-change="handleCheckChange"
  @hook:mounted="init"
></tree>
<script>
  export default {
    watch: {
      value() {
        console.log(this.value, 'value')
        // 为了检测 v-model 的变化
        if (this.value + '' !== this.selectData + '') {
          console.log('value change')
          this.treeKey = Math.random()
          // this.init()
        }
      }
    },
    methods: {
      init() {
        // this.$nextTick(() => {
        if (this.isMultiple) {
          this.handleCheckChange()
        } else {
          this.handleCurrentChange()
        }
        // })
      }
    },
    mounted() {
      // this.init()
      // 绑定 el-select 方法
      for (let key in this.$refs.select) {
        if (!(key in this) && typeof this.$refs.select[key] === 'function') {
          this[key] = this.$refs.select[key].bind(this.$refs.select)
        }
      }
    }
  }
</script>
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
35
36
37
38
39
40
41
42
43

normal

这样确实是可以了, 但会报错:

this.$refs.tree.getCurrentNode is not a function

原因是我们在 mounted 中给 Tree 添加方法的, @hook:mounted 时还没有方法, 所以报错

那就简单了, 咱们不用 hook 了, 等方法全部添加后, emit 一个事件即可

Tree.vue

mounted() {
  // 绑定 el-tree 方法
  for (let key in this.$refs[this.ref]) {
    if (!(key in this) && typeof this.$refs[this.ref][key] === 'function') {
      this[key] = this.$refs[this.ref][key].bind(this.$refs[this.ref])
    }
  }
  this.$emit('ready')
}
1
2
3
4
5
6
7
8
9

# 在 el-form 必填验证时, 多选初始化就提示必填

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

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

最近更新
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
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式