本篇文章是基于 Element-ui在此基础之上进行二次封装的
需求背景
- 在日常开发中,我们会遇见很多不同的业务需求。如果让你用element-ui实现一个 tree-select 组件,你会怎么做?
- 这个组件在 element-plus 中是有这个组件存在的,但是在 element-ui 中是没有的。
- 可能你会直接使用 element-plus 组件库,或者其他组件库。但是若你的项目目前的基于vue2和element-ui进行开发的呢?
- 下面这种思路利用 el-tree 和 el-select 进行嵌套从而实现我们想要的 tree-select 组件
最终效果
大致思路
el-select和el-tree进行嵌套,将el-tree放到el-option里,循环遍历el-option,同时定义一个方法比如:formatData,对树形数据进行递归处理,这样就可以实现无论嵌套的层级有几层都可以正常渲染在界面上
利用 v-model 和 update:selectValue 实现父子组件之间的双向通信,同时利用computed进行监听以实现实时更新
知识储备--组件中的 v-model
我们在input中可以使用v-model来完成双向绑定:
- 这个时候往往会非常方便,因为v-model默认会帮助我们完成两件事:
- v-bind:value的数据绑定和@input的事件监听;
如果我们现在封装了一个组件,其他地方在使用这个组件时,是否也可以使用v-model来同时完成这两个功能呢?
当我们在组件上使用的时候,等价于如下的操作:
- 我们会发现和input元素不同的只是属性的名称和事件触发的名称而已;
如果我们希望绑定多个属性呢?
- 也就是我们希望在一个组件上使用多个v-model是否可以实现呢?
- 我们知道,默认情况下的v-model其实是绑定了 modelValue 属性和 @update:modelValue的事件;
- 如果我们希望绑定更多,可以给v-model传入一个参数,那么这个参数的名称就是我们绑定属性的名称;
实现代码示例
子组件:
<template>
<div>
<h4>Counter: {{modelValue}}</h4>
<button @click="changeCounter">修改数据</button>
</div>
</template>
<script>
export default {
props: {
modelValue: {
type: Number
},
},
emits: ['update:modelValue'],
methods: {
changeCounter(){
this.$emit('update:modelValue',101)
}
}
}
</script>
父组件:
<template>
<!-- <child v-model="appCounter" /> -->
<!-- 等同于如下做法:modelValue--默认
可以自定义名称,通过 v-model:counter 类似于这种格式
-->
<child :modelValue="appCounter" @update:modelValue="appCounter = $event" />
</template>
<script>
import child from '@/components/child.vue'
export default {
components: {
child
},
data() {
return {
appCounter: 100,
};
},
methods: {
},
};
</script>
有了上面的知识,那么下面实现就很简单了,这里直接上代码
组件封装
子组件:TreeSelect.vue
<template>
<div class="app-container" style="padding: 0">
<el-select
class="main-select-tree"
ref="selectTree"
v-model="value"
style="width: 240px"
clearable
@clear="clearSelectInput"
>
<el-input
style="width: 220px; margin-left: 10px; margin-bottom: 10px"
placeholder="输入关键字进行过滤"
v-model="filterText"
clearable
>
</el-input>
<el-option
v-for="item in formatData(data)"
:key="item.value"
:label="item.label"
:value="item.value"
style="display: none"
/>
<el-tree
class="main-select-el-tree"
ref="selecteltree"
:data="data"
node-key="id"
highlight-current
:props="defaultProps"
@node-click="handleNodeClick"
:current-node-key="value"
:expand-on-click-node="true"
default-expand-all
:filter-node-method="filterNode"
/>
</el-select>
</div>
</template>
<script>
export default {
props: {
selectValue: {
type: String,
default: "",
},
},
data() {
return {
filterText: "",
value: "",
data: [
{
id: 1,
label: "云南",
children: [
{
id: 2,
label: "昆明",
children: [
{
id: 3,
label: "五华区",
children: [
{
id: 8,
label: "xx街道",
children: [
{
id: 81,
label: "yy社区",
children: [{ id: 82, label: "北辰小区" }],
},
],
},
],
},
{ id: 4, label: "盘龙区" },
],
},
],
},
{
id: 5,
label: "湖南",
children: [
{ id: 6, label: "长沙" },
{ id: 7, label: "永州" },
],
},
{
id: 12,
label: "重庆",
children: [
{ id: 10, label: "渝北" },
{ id: 9, label: "合川" },
],
},
{
id: 13,
label: "江苏",
children: [{ id: 14, label: "盐城" }],
},
],
defaultProps: {
children: "children",
label: "label",
},
};
},
watch: {
filterText(val) {
this.$refs.selecteltree.filter(val);
},
},
methods: {
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
// 递归遍历数据
formatData(data) {
let options = [];
const formatDataRecursive = (data) => {
data.forEach((item) => {
options.push({ label: item.label, value: item.id });
if (item.children && item.children.length > 0) {
formatDataRecursive(item.children);
}
});
};
formatDataRecursive(data);
return options;
},
// 点击事件
handleNodeClick(node) {
this.value = node.id;
this.$refs.selectTree.blur();
this.$emit('update:selectValue', node.label);
},
// 清空事件
clearSelectInput() {
this.$emit('update:selectValue', '');
// 获取 el-tree 实例的引用
const elTree = this.$refs.selecteltree;
// 将当前选中的节点设置为 null
elTree.setCurrentKey(null);
},
},
};
</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>
使用方式
<TreeSelect v-model="selectedValue" @update:selectValue="handleSelectValueChange"></TreeSelect>
<el-button size="medium" :disabled="todoIsTotal">交接当前{{ tableData.length }}条任务</el-button>
import TreeSelect from "./TreeSelect.vue";
export default {
components: {
TreeSelect,
},
data() {
selectedValue: "",
},
computed: {
todoIsTotal() {
return this.selectedValue === "";
},
},
methods: {
handleSelectValueChange(value) {
if (value && value.length > 0) {
this.selectedValue = value;
} else {
this.selectedValue = "";
}
},
},
}