介绍
vue-treeselect 是一个多选组件,具有对 Vue.js嵌套选项支持。
- 支持嵌套选项的单选和多选
- 模糊匹配
- 异步搜索
- 延迟加载(仅在需要时加载深度选项的数据)
- 键盘支持(使用Arrow Up & Arrow Down键导航,使用键选择选项Enter等)
- 丰富的选项和高度可定制的
- 支持 多种浏览器
需要Vue 2.2+
入门
建议通过npm安装vue-treeselect,并使用webpack之类的捆绑器来构建您的应用程序。
npm install --save @riophae/vue-treeselect
本示例说明如何将vue-treeselect与Vue SFCs集成在一起。
<!-- Vue SFC -->
<template>
<div id="app">
<treeselect v-model="value" :multiple="true" :options="options" />
</div>
</template>
<script>
// import the component
import Treeselect from '@riophae/vue-treeselect'
// import the styles
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
// register the component
components: { Treeselect },
data() {
return {
// define the default value
value: null,
// define options
options: [ {
id: 'a',
label: 'a',
children: [ {
id: 'aa',
label: 'aa',
}, {
id: 'ab',
label: 'ab',
} ],
}, {
id: 'b',
label: 'b',
}, {
id: 'c',
label: 'c',
} ],
}
},
}
</script>
如果您只是不想使用Webpack或任何其他捆绑程序,则只需在页面中包含独立的UMD构建即可。这样,请确保在vue-treeselect之前包括Vue作为依赖项。
<html>
<head>
<!-- include Vue 2.x -->
<script src="https://cdn.jsdelivr.net/npm/vue@^2"></script>
<!-- include vue-treeselect & its styles. you can change the version tag to better suit your needs. -->
<script src="https://cdn.jsdelivr.net/npm/@riophae/vue-treeselect@^0.4.0/dist/vue-treeselect.umd.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@riophae/vue-treeselect@^0.4.0/dist/vue-treeselect.min.css">
</head>
<body>
<div id="app">
<treeselect v-model="value" :multiple="true" :options="options" />
</div>
</body>
<script>
// register the component
Vue.component('treeselect', VueTreeselect.Treeselect)
new Vue({
el: '#app',
data: {
// define the default value
value: null,
// define options
options: [ {
id: 'a',
label: 'a',
children: [ {
id: 'aa',
label: 'aa',
}, {
id: 'ab',
label: 'ab',
} ],
}, {
id: 'b',
label: 'b',
}, {
id: 'c',
label: 'c',
} ],
},
})
</script>
</html>
文档
基本特征
此示例演示了vue-treeselect最常用的功能。通过输入几个字母来尝试模糊匹配功能。
<div>
<treeselect
:multiple="true"
:options="options"
placeholder="Select your favourite(s)..."
v-model="value"
/>
<treeselect-value :value="value" />
</div>
export default {
data: () => ({
value: [],
options: [ {
id: 'fruits',
label: 'Fruits',
children: [ {
id: 'apple',
label: 'Apple 🍎',
isNew: true,
}, {
id: 'grapes',
label: 'Grapes 🍇',
}, {
id: 'pear',
label: 'Pear 🍐',
}, {
id: 'strawberry',
label: 'Strawberry 🍓',
}, {
id: 'watermelon',
label: 'Watermelon 🍉',
} ],
}, {
id: 'vegetables',
label: 'Vegetables',
children: [ {
id: 'corn',
label: 'Corn 🌽',
}, {
id: 'carrot',
label: 'Carrot 🥕',
}, {
id: 'eggplant',
label: 'Eggplant 🍆',
}, {
id: 'tomato',
label: 'Tomato 🍅',
} ],
} ],
}),
}
您需要学习的第一件事是如何定义选项。有两种选择:一。可折叠的文件夹选项并可能有子选项,并且b。正常选项不是,不是。在这里,我想借鉴计算机科学的基本概念,将前者称为分支节点,而将后者称为叶节点。这两种节点共同构成了树。
定义叶节点非常简单:
{
id: '<id>', // used to identify the option within the tree so its value must be unique across all options
label: '<label>', // used to display the option
}
定义分支节点只需要一个额外的children
属性:
{
id: '<id>',
label: '<label>',
children: [
{
id: '<child id>',
label: '<child label>',
},
...
],
}
然后,您可以传递这些节点的数组作为options
。请注意,即使您为children
属性分配了一个空数组,它仍被视为分支节点。这可能与您从计算机科学中学到的有所不同,后者没有节点的节点通常称为叶节点。
有关node
对象中所有可用属性的信息,请参见 下文.
更多功能
这演示了更多功能。
<div>
<div :dir="rtl ? 'rtl' : 'ltr'">
<treeselect
name="demo"
:multiple="multiple"
:clearable="clearable"
:searchable="searchable"
:disabled="disabled"
:open-on-click="openOnClick"
:open-on-focus="openOnFocus"
:clear-on-select="clearOnSelect"
:close-on-select="closeOnSelect"
:always-open="alwaysOpen"
:append-to-body="appendToBody"
:options="options"
:limit="3"
:max-height="200"
v-model="value"
/>
</div>
<treeselect-value :value="value" />
<p>
<label><input type="checkbox" v-model="multiple">Multi-select</label>
<label><input type="checkbox" v-model="clearable">Clearable</label>
<label><input type="checkbox" v-model="searchable">Searchable</label>
<label><input type="checkbox" v-model="disabled">Disabled</label>
</p>
<p>
<label><input type="checkbox" v-model="openOnClick">Open on click</label>
<label><input type="checkbox" v-model="openOnFocus">Open on focus</label>
</p>
<p>
<label><input type="checkbox" v-model="clearOnSelect">Clear on select</label>
<label><input type="checkbox" v-model="closeOnSelect">Close on select</label>
</p>
<p>
<label><input type="checkbox" v-model="alwaysOpen">Always open</label>
<label><input type="checkbox" v-model="appendToBody">Append to body</label>
<label><input type="checkbox" v-model="rtl">RTL mode</label>
</p>
</div>
import { generateOptions } from './utils'
export default {
data: () => ({
multiple: true,
clearable: true,
searchable: true,
disabled: false,
openOnClick: true,
openOnFocus: false,
clearOnSelect: true,
closeOnSelect: false,
alwaysOpen: false,
appendToBody: false,
rtl: false,
value: [ 'a' ],
options: generateOptions(2, 3),
}),
watch: {
multiple(newValue) {
if (newValue) {
this.value = this.value ? [ this.value ] : []
} else {
this.value = this.value[0]
}
},
},
}
延迟加载
如果您有大量深度嵌套的选项,则可能只希望在初始加载时加载最顶层的选项,而仅在需要时加载其余选项。您可以通过执行以下步骤来实现:
- 通过设置声明一个卸载的分支节点
children: null
- 添加
loadOptions
道具 - 每当卸载的分支节点被扩展时,
loadOptions({ action, parentNode, callback, instanceId })
都会被调用,然后您就可以执行从远程服务器请求数据的作业
<treeselect
:multiple="true"
:options="options"
:load-options="loadOptions"
placeholder="Try expanding any folder option..."
v-model="value"
/>
import { LOAD_CHILDREN_OPTIONS } from '@riophae/vue-treeselect'
// We just use `setTimeout()` here to simulate an async operation
// instead of requesting a real API server for demo purpose.
const simulateAsyncOperation = fn => {
setTimeout(fn, 2000)
}
export default {
data: () => ({
value: null,
options: [ {
id: 'success',
label: 'With children',
// Declare an unloaded branch node.
children: null,
}, {
id: 'no-children',
label: 'With no children',
children: null,
}, {
id: 'failure',
label: 'Demonstrates error handling',
children: null,
} ],
}),
methods: {
loadOptions({ action, parentNode, callback }) {
// Typically, do the AJAX stuff here.
// Once the server has responded,
// assign children options to the parent node & call the callback.
if (action === LOAD_CHILDREN_OPTIONS) {
switch (parentNode.id) {
case 'success': {
simulateAsyncOperation(() => {
parentNode.children = [ {
id: 'child',
label: 'Child option',
} ]
callback()
})
break
}
case 'no-children': {
simulateAsyncOperation(() => {
parentNode.children = []
callback()
})
break
}
case 'failure': {
simulateAsyncOperation(() => {
callback(new Error('Failed to load options: network error.'))
})
break
}
default: /* empty */
}
}
},
},
}
还可以延迟延迟加载根级别选项。如果最初没有注册任何选项(options: null
),则vue-treeselect将loadOptions({ action, callback, instanceId })
在组件安装后通过调用来尝试加载根选项。在此示例中,我通过设置禁用了自动加载功能autoLoadRootOptions: false
,并且在打开菜单时将加载根选项。
<treeselect
:load-options="loadOptions"
:options="options"
:auto-load-root-options="false"
:multiple="true"
placeholder="Open the menu..."
/>
import { LOAD_ROOT_OPTIONS } from '@riophae/vue-treeselect'
const sleep = d => new Promise(r => setTimeout(r, d))
let called = false
export default {
data: () => ({
options: null,
}),
methods: {
// You can either use callback or return a Promise.
async loadOptions({ action/*, callback*/ }) {
if (action === LOAD_ROOT_OPTIONS) {
if (!called) {
// First try: simulate an exception.
await sleep(2000) // Simulate an async operation.
called = true
throw new Error('Failed to load options: test.')
} else {
// Second try: simulate a successful loading.
await sleep(2000)
this.options = [ 'a', 'b', 'c', 'd', 'e' ].map(id => ({
id,
label: `option-${id}`,
}))
}
}
},
},
}
异步搜索
vue-treeselect支持根据用户类型动态加载和更改整个选项列表。默认情况下,vue-treeselect将缓存每个AJAX请求的结果,因此用户可以减少等待时间。
<treeselect
:multiple="true"
:async="true"
:load-options="loadOptions"
/>
import { ASYNC_SEARCH } from '@riophae/vue-treeselect'
const simulateAsyncOperation = fn => {
setTimeout(fn, 2000)
}
export default {
methods: {
loadOptions({ action, searchQuery, callback }) {
if (action === ASYNC_SEARCH) {
simulateAsyncOperation(() => {
const options = [ 1, 2, 3, 4, 5 ].map(i => ({
id: `${searchQuery}-${i}`,
label: `${searchQuery}-${i}`,
}))
callback(null, options)
})
}
},
},
}
平面模式和排序值
在前面的所有示例中,我们使用了默认的非平坦模式vue-treeselect,这意味着:
- 每当分支节点被检查时,其所有子节点也将被检查
- 每当分支节点检查所有子节点时,分支节点本身也将被检查
有时我们不需要那种机制,并且希望分支节点和叶子节点不会相互影响。在这种情况下,应使用平面模式,如下所示。
如果要控制所选选项的显示顺序,请使用sortValueBy
道具。该道具有三个选择:
"ORDER_SELECTED"
(默认)-选择订单"LEVEL"
- 选择级别: C 🡒 BB 🡒 AAA"INDEX"
- 选项索引: AAA 🡒 BB 🡒 C
<div>
<treeselect
:multiple="true"
:options="options"
:flat="true"
:sort-value-by="sortValueBy"
:default-expand-level="1"
placeholder="Try selecting some options."
v-model="value"
/>
<treeselect-value :value="value" />
<p><strong>Sort value by:</strong></p>
<p class="options">
<label><input type="radio" value="ORDER_SELECTED" v-model="sortValueBy">Order selected</label>
<label><input type="radio" value="LEVEL" v-model="sortValueBy">Level</label>
<label><input type="radio" value="INDEX" v-model="sortValueBy">Index</label>
</p>
</div>
import { generateOptions } from './utils'
export default {
data() {
return {
value: [ 'c', 'aaa', 'bb' ],
options: generateOptions(3),
sortValueBy: 'ORDER_SELECTED',
}
},
}
防止价值组合
对于非固定和多选模式,如果选中了分支节点及其所有后代,则vue-treeselect会将它们组合到值数组中的单个项目中,如以下示例所示。通过使用valueConsistsOf
道具,您可以更改该行为。该道具有四个选项:
"ALL"
- 选中的所有节点都将包含在value
数组中"BRANCH_PRIORITY"
(默认)-如果选中了分支节点,则其所有后代将被排除在value
数组之外"LEAF_PRIORITY"
- 如果选中了分支节点,则此节点本身及其分支后代将从value
阵列中排除,但其叶后代将包括在内"ALL_WITH_INDETERMINATE"
-选中的任何节点将包括在value
数组中,另外还有不确定的节点
<div>
<treeselect
:multiple="true"
:options="options"
:value-consists-of="valueConsistsOf"
v-model="value"
/>
<treeselect-value :value="value" />
<p><strong>Value consists of:</strong></p>
<p class="options">
<label><input type="radio" value="ALL" v-model="valueConsistsOf">All</label><br>
<label><input type="radio" value="BRANCH_PRIORITY" v-model="valueConsistsOf">Branch priority</label><br>
<label><input type="radio" value="LEAF_PRIORITY" v-model="valueConsistsOf">Leaf priority</label><br>
<label><input type="radio" value="ALL_WITH_INDETERMINATE" v-model="valueConsistsOf">All with indeterminate</label>
</p>
</div>
export default {
data: () => ({
value: [ 'team-i' ],
valueConsistsOf: 'BRANCH_PRIORITY',
options: [ {
id: 'company',
label: 'Company 🏢',
children: [ {
id: 'team-i',
label: 'Team I 👥',
children: [ {
id: 'person-a',
label: 'Person A 👱',
}, {
id: 'person-b',
label: 'Person B 🧔',
} ],
}, {
id: 'team-ii',
label: 'Team II 👥',
children: [ {
id: 'person-c',
label: 'Person C 👳',
}, {
id: 'person-d',
label: 'Person D 👧',
} ],
}, {
id: 'person-e',
label: 'Person E 👩',
} ],
} ],
}),
}
禁用分支节点
设置 disableBranchNodes: true
为使分支节点不可检查,并将其仅视为可折叠文件夹。通过设置,您可能还想在每个分支节点的标签旁边显示一个计数showCount: true
.
<treeselect
:options="options"
:disable-branch-nodes="true"
:show-count="true"
placeholder="Where are you from?"
/>
import countries from './data/countries-of-the-world'
export default {
data: () => ({
options: countries,
}),
}
拼合搜索结果
设置 flattenSearchResults: true
为在搜索时展平树。将此选项设置true
为时,将仅显示匹配的结果。将此设置为false
(默认)后,即使其祖先未单独包括在结果中,也将显示其祖先。
<treeselect
:options="options"
:multiple="true"
:flatten-search-results="true"
placeholder="Where are you from?"
/>
import countries from './data/countries-of-the-world'
export default {
data: () => ({
options: countries,
}),
}
禁用项目选择
您可以通过isDisabled: true
在任何叶节点或分支节点上进行设置来禁用项目选择。对于非扁平模式,在分支节点上设置也会禁用其所有后代。
<treeselect
:multiple="true"
:options="options"
:value="value"
/>
export default {
data: () => ({
options: [ {
id: 'folder',
label: 'Normal Folder',
children: [ {
id: 'disabled-checked',
label: 'Checked',
isDisabled: true,
}, {
id: 'disabled-unchecked',
label: 'Unchecked',
isDisabled: true,
}, {
id: 'item-1',
label: 'Item',
} ],
}, {
id: 'disabled-folder',
label: 'Disabled Folder',
isDisabled: true,
children: [ {
id: 'item-2',
label: 'Item',
}, {
id: 'item-3',
label: 'Item',
} ],
} ],
value: [ 'disabled-checked' ],
}),
}
巢状搜寻
有时我们需要在特定分支中搜索选项的可能性。例如,您的分支机构是不同的餐馆,叶子是他们点的食物。要搜索“ McDonals”餐厅的沙拉订单,只需搜索“ mc salad”。您也可以尝试搜索 "salad"来感受不同。
具体地说,您的搜索查询会在空格上分开。如果在节点路径中找到每个分割的字符串,则我们有一个匹配项。
<treeselect
:multiple="true"
:options="options"
:disable-branch-nodes="true"
v-model="value"
search-nested
/>
export default {
data: () => ({
value: [],
options: [ {
id: 'm',
label: 'McDonalds',
children: [ {
id: 'm-fries',
label: 'French Fries',
}, {
id: 'm-cheeseburger',
label: 'Cheeseburger',
}, {
id: 'm-white-cheedar-burger',
label: 'White Cheddar Burger',
}, {
id: 'm-southwest-buttermilk-crispy-chicken-salad',
label: 'Southwest Buttermilk Crispy Chicken Salad',
}, {
id: 'm-cola',
label: 'Coca-Cola®',
}, {
id: 'm-chocolate-shake',
label: 'Chocolate Shake',
} ],
}, {
id: 'kfc',
label: 'KFC',
children: [ {
id: 'kfc-fries',
label: 'French Fries',
}, {
id: 'kfc-chicken-litties-sandwiches',
label: 'Chicken Litties Sandwiches',
}, {
id: 'kfc-grilled-chicken',
label: 'Grilled Chicken',
}, {
id: 'kfc-cola',
label: 'Pepsi® Cola',
} ],
}, {
id: 'bk',
label: 'Burger King',
children: [ {
id: 'bk-chicken-fries',
label: 'Chicken Fries',
}, {
id: 'bk-chicken-nuggets',
label: 'Chicken Nuggets',
}, {
id: 'bk-garden-side-salad',
label: 'Garden Side Salad',
}, {
id: 'bk-cheeseburger',
label: 'Cheeseburger',
}, {
id: 'bk-bacon-king-jr-sandwich',
label: 'BACON KING™ Jr. Sandwich',
}, {
id: 'bk-cola',
label: 'Coca-Cola®',
}, {
id: 'bk-oreo-chocolate-shake',
label: 'OREO® Chocolate Shake',
} ],
} ],
}),
}
该模式禁用了模糊匹配功能,以避免不匹配。
自定义键名
如果通过AJAX加载的选项数据与vue-treeselect要求的数据结构不同,例如,您的数据具有name
属性,但vue-treeselect需要label
,则可能需要自定义键名。在这种情况下,您可以提供一个称为函数的propnormalizer
,在数据初始化期间它将传递给树中的每个节点。使用此函数创建并返回转换后的对象。
<treeselect
:options="options"
:value="value"
:normalizer="normalizer"
/>
export default {
data: () => ({
value: null,
options: [ {
key: 'a',
name: 'a',
subOptions: [ {
key: 'aa',
name: 'aa',
} ],
} ],
normalizer(node) {
return {
id: node.key,
label: node.name,
children: node.subOptions,
}
},
}),
}
自定义选项标签
您可以自定义每个选项的标签。vue-treeselect利用了Vue的作用域插槽功能,并提供了一些您应该在自定义模板中使用的道具:
node
-标准化的节点对象(请注意,这与您从normalizer()
返回的内容不同)count
&shouldShowCount
- 计数数字和布尔值指示是否应显示计数labelClassName
&countClassName
- CSS类名,用于使样式正确
<treeselect
:options="options"
:value="value"
:searchable="false"
:show-count="true"
:default-expand-level="1"
>
<label slot="option-label" slot-scope="{ node, shouldShowCount, count, labelClassName, countClassName }" :class="labelClassName">
{{ node.isBranch ? 'Branch' : 'Leaf' }}: {{ node.label }}
<span v-if="shouldShowCount" :class="countClassName">({{ count }})</span>
</label>
</treeselect>
import { generateOptions } from './utils'
export default {
data: () => ({
value: null,
options: generateOptions(2),
}),
}
自定义值标签
您可以自定义值项的标签(在多选的情况下,每个项)。vue-treeselect利用了Vue的作用域插槽 功能,并提供了一些您应该在自定义模板中使用的道具:
node
- 标准化的节点对象(请注意,这与您从normalizer()
返回的内容不同)
<div>
<treeselect :options="options" :value="value" :multiple="multiple">
<div slot="value-label" slot-scope="{ node }">{{ node.raw.customLabel }}</div>
</treeselect>
<p>
<label><input type="checkbox" v-model="multiple">Multi-select</label>
</p>
</div>
export default {
data: () => ({
multiple: true,
value: null,
options: [ 1, 2, 3 ].map(i => ({
id: i,
label: `Label ${i}`,
customLabel: `Custom Label ${i}`,
})),
}),
}
Vuex支持
在之前的所有示例中,我们v-model
都曾在应用程序状态和vue-treeselect之间(也称为双向数据绑定)之间同步值。如果您使用的是Vuex,我们可以使用:value
和 @input
,因为v-model
这只是Vue 2中它们的语法糖。
具体而言,我们可以绑定getter
到 :value
使VUE-treeselect反映您Vuex状态的任何变化,并结合action
或 mutation
以 @input
更新您的Vuex状态每当值改变。
<div>
<treeselect
:options="options"
:value="value"
:searchable="false"
@input="updateValue"
/>
<treeselect-value :value="value" />
</div>
import Vue from 'vue'
import Vuex, { mapState, mapMutations } from 'vuex'
import { generateOptions } from './utils'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
value: 'a',
},
mutations: {
updateValue(state, value) {
state.value = value
},
},
})
export default {
store,
data: () => ({
options: generateOptions(2),
}),
computed: {
...mapState([ 'value' ]),
},
methods: {
...mapMutations([ 'updateValue' ]),
},
}