Preface
In
vue
, if you want to implement a drop-down tree component, you can directly use the componentelement plus
intreeSelect
, but if your project is usingelement 2.X
the version, then it does not containtreeSelect
components, but we can still use some third-party plug-ins or Encapsulate the component yourself to achieve this operation.
1. @riophae/vue-treeselect plug-in
riophae/vue-treeselect is a vue.js
plug-in based on , which provides a tree selector component for selecting tree-structured data. The plug-in supports multiple selection, search, asynchronous loading and other functions, and can customize the style and template of options. Its ease of use and scalability make it suitable for all types of projects.
1.1 Installation
npm/cnpm
oryarn
install
npm i @riophae/vue-treeselect
yarn add @riophae/vue-treeselect
1.2 Introduction
Entry file
main.js
globally imported
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
Vue.component('treeselect', Treeselect)
Use file local import
<script>
import Treeselect from '@riophae/vue-treeselect'
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
components: {
Treeselect
},
}
</script>
1.3 Basic usage
<template>
<div class="box">
<treeselect v-model="selectedItems" placeholder="请选择" :options="treeData"></treeselect>
<el-button @click="getSelectedItems">获取选中的数据</el-button>
</div>
</template>
<script>
export default {
data() {
return {
selectedItems: null,
treeData: [
{
id: 1,
label: "水果",
children: [
{
id: 2, label: "西瓜" },
{
id: 3, label: "香蕉" },
{
id: 4, label: "橙子" },
],
},
{
id: 5,
label: "蔬菜",
children: [
{
id: 6, label: "西红柿" },
{
id: 7, label: "黄瓜" },
{
id: 8, label: "青菜" },
],
},
{
id: 9,
label: "零食",
children: [
{
id: 10, label: "薯片" },
{
id: 11, label: "巧克力" },
],
},
],
};
},
methods: {
getSelectedItems() {
console.log(this.selectedItems);
},
},
};
</script>
achieve effect
1.4 Advanced use
- Commonly used properties
Attributes | describe |
---|---|
v-model=“value” | Use the v-model directive to bind the selected item |
:options=“treeData” | Set tree data |
:multiple=“true” | Allow multiple selections |
:clearable=“true” | Show clear button |
:searchable=“true” | Show search box |
:disabled=“false” | Whether to disable |
:openOnFocus=“true” | Automatically expand drop-down menu when focused |
:openOnClick=“true” | Automatically expand drop-down menu on click |
:auto-load-root-options=“true” | Automatically load root-level options |
:async=“true” | Asynchronous loading options |
:load-options=“loadOptions” | How to load options asynchronously |
:noChildrenText="'No sub-options'" | Text prompt when there are no sub-options |
:noOptionsText="'No options'" | Text prompt when there are no options |
:noResultsText="'No matching results'" | Text prompt when search results are empty |
:placeholder="'Please select'" | placeholder text |
:appendToBody=“true” | Whether the popup box is attached to the body element |
:normalizer=“normalizeOptions” | Normalize option data. Through the normalizer attribute, you can customize the structure of the option data to adapt to the requirements of the plug-in |
valueFormat=“” | Able to determine the format of the value attribute. When set to "id", the format of the value attribute is id or an array of ids. When set to "object", the format of the value attribute is node or node array |
- Commonly used methods
method | describe |
---|---|
@open=“handleOpen” | Event triggered when drop-down menu is opened |
@close=“handleClose” | Event triggered when drop-down menu is closed |
@deselect=“handleRemove” | Event triggered when the selected item is removed |
@search-change=“handleSearch” | Events triggered when searching |
@select=“handleSelect” | Event triggered when an item is selected (not triggered when a value is cleared) |
@input=“handleInput” | Selected trigger (the first echo will trigger, the clear value will trigger, the value is undefined) is mostly used for v-model two-way binding components to update parent components. |
- Practice code
<template>
<div>
<treeselect v-model="selectedItems" :options="treeData" :multiple="true" :clearable="true" :searchable="true" :disabled="false"
:openOnFocus="true" :openOnClick="true" :auto-load-root-options="true" :async="false" :load-options="loadOptions"
:noChildrenText="'没有子选项'" :noOptionsText="'没有可选项'" :noResultsText="'没有匹配的结果'" :placeholder="'请选择'" :appendToBody="true"
@open="handleOpen" @close="handleClose" @input="handleInput" @deselect="handleRemove" @search-change="handleSearch"
@select="handleSelect"></treeselect>
<button @click="getSelectedItems">获取选中的数据</button>
</div>
</template>
<script>
export default {
data() {
return {
selectedItems: [], // 选中的项
treeData: [
{
id: 1,
label: "水果",
children: [
{
id: 2, label: "西瓜" },
{
id: 3, label: "香蕉" },
{
id: 4, label: "橙子" },
],
},
{
id: 5,
label: "蔬菜",
children: [
{
id: 6, label: "西红柿" },
{
id: 7, label: "黄瓜" },
{
id: 8, label: "青菜" },
],
},
{
id: 9,
label: "零食",
children: [
{
id: 10, label: "薯片" },
{
id: 11, label: "巧克力" },
],
},
],
};
},
methods: {
loadOptions({
parentNode, callback }) {
// 异步加载选项的方法
// parentNode: 当前父节点
// callback: 加载完成后的回调函数
// 在这里根据需要进行异步加载选项的操作,并在加载完成后调用callback方法传递选项数据
},
handleOpen() {
// 下拉菜单打开时触发的事件
console.log("下拉菜单打开");
},
handleClose() {
// 下拉菜单关闭时触发的事件
console.log("下拉菜单关闭");
},
handleRemove(removedItem) {
// 移除选中项时触发的事件
console.log("移除选中项", removedItem);
},
handleSearch(searchText) {
// 搜索时触发的事件
console.log("搜索", searchText);
},
handleSelect(selectedItems) {
// 选择项时触发的事件
console.log("选择项select", selectedItems);
},
handleInput(selectedItems) {
// 选择项时触发的事件
console.log("选择项input", selectedItems);
},
getSelectedItems() {
// 获取选中的数据
console.log(this.selectedItems);
},
},
};
</script>
1.5 FAQ
1.5.1 Placeholders
unknown
-
Screenshot of problem
-
Solution
v-model
It cannot be written as an empty string or empty array, otherwise it will appearunknown
. You can default to itnull
.
1.5.2 Data prompt in English
- Screenshot of problem
-
Solution
Use the
noChildrenText
,noOptionsText
andnoResultsText
properties of custom text.noChildrenText
: Used to define the text prompt when an option has no sub-options. For example, when a category has no subcategories, you can usenoChildrenText
to display the corresponding prompt text.
noOptionsText
: Used to define text prompts when there are no options.noOptionsText
For example, you can use to display the corresponding prompt text when the data is empty or there are no matching options .
noResultsText
: Used to define text prompts when the search results are empty. For example, when the user performs a search but no matching results are found, you can usenoResultsText
to display the corresponding prompt text.<treeselect v-model="selectedItems" :options="treeData" noChildrenText="没有子选项" noOptionsText="没有可选项" noResultsText="没有匹配的结果"></treeselect>
1.5.3 Data structure does not comply with
-
Problem Description
Many times, the fields of the data structure returned by the background are not
id
,label
,children
and these. At this time, we need to replace it with a data structure that meets the requirements. -
Solution
Use
normalizer
the attribute, which is used to normalize option data. Throughnormalizer
properties, you can customize the structure of the options data to suit the plug-in's requirements.<template> <div class="box"> <treeselect v-model="selectedItems" :normalizer="normalizeOptions" :options="treeData"></treeselect> </div> </template> <script> export default { data() { return { selectedItems: null, treeData: [ { id: 1, name: "水果", children: [ { id: 2, name: "苹果", children: [ { id: 21, name: "红苹果" }, { id: 22, name: "绿苹果" }, ], }, { id: 3, name: "香蕉", children: [ { id: 31, name: "大香蕉" }, { id: 32, name: "小香蕉" }, ], }, ], }, { id: 5, name: "蔬菜", children: [ { id: 6, name: "叶菜类", children: [ { id: 61, name: "菠菜" }, { id: 62, name: "生菜" }, ], }, { id: 7, name: "根茎类", children: [], }, ], }, ], }; }, methods: { // 规范化选项数据的方法 normalizeOptions(node) { // node: 原始的选项数据 // 在这里根据需要进行选项数据的规范化操作,并返回规范化后的选项数据 // 例如,可以将原始的选项数据转换为符合插件要求的结构 if (node.children && !node.children.length) { // 去掉children=[]的children属性 delete node.children; } return { id: node.id, label: node.name, children: node.children, }; }, }, }; </script>
Of course, you can also implement it manually, recursively.
<template> <div class="box"> <treeselect v-model="selectedItems" :options="treeData"></treeselect> </div> </template> <script> export default { data() { return { selectedItems: null, treeData: [], }; }, mounted() { let list = [ { id: 1, name: "层级1", children: [ { id: 2, name: "层级2", children: [ { id: 3, name: "层级3", children: [ { id: 4, name: "层级4", }, ], }, ], }, ], }, ]; this.treeData = this.normalizeOptions(list); }, methods: { normalizeOptions(options) { const normalizedOptions = []; if (options) { for (const option of options) { // 创建一个规范化选项对象,将id和name属性映射到该对象中 const normalizedOption = { id: option.id, label: option.name, }; // 检查当前选项是否有子选项 if (option.children && option.children.length > 0) { // 如果有子选项,递归调用normalizeOptions方法对子选项进行规范化 // 并将规范化后的子选项数组赋值给当前选项的children属性 normalizedOption.children = this.normalizeOptions(option.children); } // 将规范化后的选项对象添加到normalizedOptions数组中 normalizedOptions.push(normalizedOption); } } return normalizedOptions; }, }, }; </script>
1.5.4 Style adjustment
/* 组件样式 */
::v-deep .vue-treeselect {
width: 200px;
height: 30px;
line-height: 30px;
font-size: 18px;
}
/* 内容样式 */
::v-deep .vue-treeselect__control {
height: 30px;
color: blue;
}
/* 占位符样式 */
::v-deep .vue-treeselect__placeholder,
::v-deep .vue-treeselect__single-value {
color: red;
}
1.5.5 Get the selected node object instead of a single value
valueFormat
Properties determine value
the format of the property. When set to id
, value
the format of the attribute is id
an or id
array. When set to object
, value
the format of the attribute is node
an or node
array.
-
single value
-
node object
<treeselect v-model="selectedItems" valueFormat="object" :options="treeData" @input="handleInput"></treeselect>
2. Custom components
Custom properties and methods
Properties/Methods | describe | type |
---|---|---|
data | display data | array |
props | Configuration options. For specific configuration, please refer to the configuration of el-tree in the element ui library. | object |
show-checkbox | Whether the node can be selected | boolean |
check-strictly | When the check box is displayed, whether to strictly follow the practice of parent and child not being related to each other, the default is false | boolean |
icon-class | Customize icons for tree nodes | string |
load | Method for loading subtree data, only effective when lazy attribute is true | function(node, resolve) |
lazy | Whether to lazily load child nodes needs to be used in conjunction with the load method. | boolean |
disabled | Whether the drop-down box is disabled | boolean |
getCheckedKeys | If the node can be selected (that is, show-checkbox is true), an array consisting of the keys of the currently selected node is returned. | – |
getCurrentNode | Get the data of the currently selected node. If no node is selected, return null. | – |
collapse-tags | Whether to display the selected value as text when making multiple selections | – |
select-last-node | Whether only the last node can be selected during single selection | – |
show-count | If there are children in the node, the number of children will be displayed on the parent node. Note that show-count will be invalid when setting the slot. | – |
clearable | Is it possible to clear the options when making a single selection? | boolean |
filterable | Enable search | boolean |
Package file
<template>
<el-select :value="valueFilter(value)" :placeholder="$attrs['placeholder']" :multiple="$attrs['show-checkbox']"
:disabled="$attrs['disabled']" :filterable="$attrs['filterable']" :clearable="$attrs['clearable']"
:collapse-tags="$attrs['collapse-tags']" @change="selectChange" @clear="selectClear" ref="mySelect" :filter-method="remoteMethod">
<template slot="empty">
<div class="selecTree">
<el-tree :data="data" :props="props" @node-click="handleNodeClick" :show-checkbox="$attrs['show-checkbox']"
:check-strictly="$attrs['check-strictly']" :icon-class="$attrs['icon-class']" :lazy="$attrs['lazy']" :load="$attrs['load']"
:node-key="props.value" :filter-node-method="filterNode" @check-change="handleCheckChange"
:default-expanded-keys="defaultExpandedKeys" ref="myTree">
<template slot-scope="{ node, data }">
<slot :node="node" :data="data">
<span class="slotSpan">
<span>
{
{ data[props.label] }}
<b v-if="$attrs['show-count'] != undefined && data[props.children]">({
{ data[props.children].length }})</b>
</span>
</span>
</slot>
</template>
</el-tree>
</div>
</template>
</el-select>
</template>
<script>
export default {
props: {
value: {
type: undefined,
default: null,
},
data: {
type: Array,
default: null,
},
props: {
type: Object,
default: null,
},
},
data() {
return {
defaultExpandedKeys: [],
};
},
created() {
this.propsInit();
},
mounted() {
setTimeout(this.initData, 10);
},
beforeUpdate() {
this.propsInit();
this.initData();
},
methods: {
initData() {
if (this.$attrs["show-checkbox"] === undefined) {
let newItem = this.recurrenceQuery(
this.data,
this.props.value,
this.value
);
if (newItem.length) {
if (this.props.value && newItem[0][this.props.value]) {
this.defaultExpandedKeys = [newItem[0][this.props.value]];
}
this.$nextTick(() => {
this.$refs.myTree.setCurrentNode(newItem[0]);
});
}
} else {
let newValue = JSON.parse(JSON.stringify(this.value));
if (!(newValue instanceof Array)) {
newValue = [newValue];
}
if (newValue?.length) {
let checkList = newValue.map((key) => {
if (key) {
let newItem = this.recurrenceQuery(
this.data,
this.props.value,
key
);
return newItem[0] || "";
}
});
if (checkList?.length) {
let defaultExpandedKeys = checkList.map(
(item) => item?.[this.props.value || ""]
);
if (defaultExpandedKeys.length)
this.defaultExpandedKeys = defaultExpandedKeys;
this.$nextTick(() => {
this.$refs.myTree.setCheckedNodes(checkList);
});
}
}
}
this.$forceUpdate();
},
// 多选
handleCheckChange(data, e, ev) {
let checkList = this.$refs.myTree.getCheckedNodes();
let setList = null;
if (checkList.length) {
setList = checkList.map((item) => item[this.props.value]);
}
this.$emit("input", setList);
// 共三个参数,依次为:传递给 data 属性的数组中该节点所对应的对象、节点本身是否被选中、节点的子树中是否有被选中的节点
this.$emit("change", data, e, ev);
},
// 单选事件
handleNodeClick(data, e) {
if (!(this.$attrs["select-last-node"] === undefined)) {
if (data[this.props.children] && data[this.props.children]?.length) {
return false;
}
}
if (this.$attrs["show-checkbox"] === undefined) {
this.$emit("input", data[this.props.value]);
this.$refs.mySelect.blur();
}
this.$emit("change", data, e);
},
// 递归查找通用方法
recurrenceQuery(list, key, value) {
if (!list || !key || !value) return [];
let queryData = [];
list.map((item) => {
if (item[this.props.children] && item[this.props.children].length) {
queryData.push(
...this.recurrenceQuery(item[this.props.children], key, value)
);
}
if (item[key] == value) {
queryData.push(item);
}
return item;
});
return queryData;
},
selectChange(e) {
if (this.$attrs["show-checkbox"] !== undefined) {
let checkList = e.map((key) => {
let newItem = this.recurrenceQuery(this.data, this.props.label, key);
return newItem[0] || "";
});
this.$refs.myTree.setCheckedNodes(checkList);
this.$emit("input", e);
}
},
selectClear(flag) {
if (this.$attrs["show-checkbox"] === undefined) {
if (!flag) this.$emit("input", "");
this.$refs.myTree.setCurrentKey(null);
} else {
if (!flag) this.$emit("input", []);
this.$refs.myTree.setCheckedKeys([]);
}
this.remoteMethod("");
},
getCheckedNodes() {
if (
this.value !== null &&
this.value !== undefined &&
this.value !== ""
) {
return this.$refs.myTree.getCheckedNodes();
}
return [];
},
getCurrentNode() {
if (
this.value !== null &&
this.value !== undefined &&
this.value !== ""
) {
return this.$refs.myTree.getCurrentNode();
}
return null;
},
valueFilter(val) {
if (this.$attrs["show-checkbox"] === undefined) {
let res = "";
[res] = this.recurrenceQuery(this.data, this.props.value, val);
return res?.[this.props.label] || "";
} else {
if (!val?.length) return [];
let res = val.map((item) => {
let [newItem] = this.recurrenceQuery(
this.data,
this.props.value,
item
);
return newItem?.[this.props.label] || "";
});
if (!res?.length) return [];
res = res.filter((item) => item);
return res;
}
},
propsInit() {
this.props.label = this.props.label || "label";
this.props.value = this.props.value || "value";
this.props.children = this.props.children || "children";
if (
this.$attrs["select-last-node"] !== undefined &&
!this.props.disabled
) {
this.props.disabled = (data) => data?.[this.props.children]?.length;
this.$attrs["check-strictly"] = true;
}
},
remoteMethod(query) {
this.$refs.myTree.filter(query);
},
filterNode(value, data) {
if (!value) return true;
return data[this.props.label].indexOf(value) !== -1;
},
},
watch: {
value: {
deep: true,
handler(val) {
if (!val || !val?.length) {
this.selectClear(true);
}
},
},
},
};
</script>
working with files
<template>
<div class="box">
<tree-select @change="sendSelectedValue" v-model="value" :data="treeData" :props="treeProps" filterable clearable></tree-select>
</div>
</template>
<script>
import TreeSelect from "@/components/treeSelect";
export default {
components: {
TreeSelect,
},
data() {
return {
value: "",
treeData: [
{
id: 1,
name: "水果",
children: [
{
id: 2,
name: "苹果",
children: [
{
id: 21, name: "红苹果" },
{
id: 22, name: "绿苹果" },
],
},
{
id: 3,
name: "香蕉",
children: [
{
id: 31, name: "大香蕉" },
{
id: 32, name: "小香蕉" },
],
},
],
},
{
id: 5,
name: "蔬菜",
children: [
{
id: 6,
name: "叶菜类",
children: [
{
id: 61, name: "菠菜" },
{
id: 62, name: "生菜" },
],
},
{
id: 7,
name: "根茎类",
children: [],
},
],
},
],
// 配置项
treeProps: {
label: "name", // 树节点的文本字段
value: "id", // 树节点的值字段
children: "children", // 树节点的子节点字段
disabled: (data) => data.disabled, // 禁用节点的条件函数,接收一个参数 data,返回一个布尔值
iconClass: "custom-icon", // 自定义树节点的图标样式
checkStrictly: true, // 在显示复选框的情况下,是否严格遵循父子节点不互相关联的做法
load: (node, resolve) => {
}, // 加载子树数据的方法,仅当 lazy 属性为 true 时生效
lazy: true, // 是否懒加载子节点,需与 load 方法结合使用
collapseTags: true, // 多选时是否将选中值按文字的形式展示
selectLastNode: true, // 单选时是否只能选择最后一个节点
showCount: true, // 若节点中存在 children,则在父节点展示所属 children 的数量
},
};
},
methods: {
sendSelectedValue(e) {
console.log(e);
},
},
};
</script>
achieve effect