Introduction
vue-treeselect is a multi-select component with nested options support for Vue.js.
- Single & multiple select with nested options support
- Fuzzy matching
- Async searching
- Delayed loading (load data of deep level options only when needed)
- Keyboard support (navigate using Arrow Up & Arrow Down keys, select option using Enter key, etc.)
- Rich options & highly customizable
- Supports a wide range of browsers
Requires Vue 2.2+
Getting Started
It's recommended to install vue-treeselect via npm, and build your app using a bundler like webpack.
npm install --save @riophae/vue-treeselect
This example shows how to integrate vue-treeselect with your 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>
If you just don't want to use webpack or any other bundlers, you can simply include the standalone UMD build in your page. In this way, make sure Vue as a dependency is included before vue-treeselect.
<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>
Guides
Basic Features
This example demonstrates the most commonly-used features of vue-treeselect. Try the fuzzy matching functionality by typing a few letters.
<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 🍅',
} ],
} ],
}),
}
The first thing you need to learn is how to define options. There are two types of options: a. folder options that are foldable & may have children options, and b. normal options that aren't & don't. Here, I'd like to borrow the basic concepts from Computer Science and call the former as branch nodes & the latter as leaf nodes. These two kinds of nodes together compose the tree.
Defining leaf nodes is quite simple:
{
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
}
Defining branch nodes only needs an extra children
property:
{
id: '<id>',
label: '<label>',
children: [
{
id: '<child id>',
label: '<child label>',
},
...
],
}
Then you can pass an array of these nodes as the options
prop. Note that, even if you assign an empty array to the children
property, it's still considered to be a branch node. This is likely different from what you've learnt from Computer Science, in which a node with no children is commonly known as a leaf node.
For information on all available properties in a node
object, see below.
More Features
This demonstrates more features.
<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]
}
},
},
}
Delayed Loading
If you have a large number of deeply nested options, you might want to load options only of the most top levels on initial load, and load the rest only when needed. You can achieve that by following these steps:
- Declare an unloaded branch node by setting
children: null
- Add
loadOptions
prop - Whenever an unloaded branch node gets expanded,
loadOptions({ action, parentNode, callback, instanceId })
will be called, then you can perform the job requesting data from a remote server
<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 */
}
}
},
},
}
It's also possible to have root level options to be delayed loaded. If no options have been initially registered (options: null
), vue-treeselect will attempt to load root options by calling loadOptions({ action, callback, instanceId })
after the component is mounted. In this example I have disabled the auto loading feature by setting autoLoadRootOptions: false
, and the root options will be loaded when the menu is opened.
<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}`,
}))
}
}
},
},
}
Async Searching
vue-treeselect supports dynamically loading & changing the entire options list as the user types. By default, vue-treeselect will cache the result of each AJAX request, thus the user could wait less.
<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)
})
}
},
},
}
Flat Mode & Sort Values
In all previous examples, we used the default non-flat mode of vue-treeselect, which means:
- Whenever a branch node gets checked, all its children will be checked too
- Whenever a branch node has all children checked, the branch node itself will be checked too
Sometimes we don't need that mechanism, and want branch nodes & leaf nodes don't affect each other. In this case, flat mode should be used, as demonstrated in the following.
If you want to control the order in which selected options to be displayed, use the sortValueBy
prop. This prop has three options:
"ORDER_SELECTED"
(default) - Order selected"LEVEL"
- Level of option: C 🡒 BB 🡒 AAA"INDEX"
- Index of option: 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',
}
},
}
Prevent Value Combining
For non-flat & multi-select mode, if a branch node and its all descendants are checked, vue-treeselect will combine them into a single item in the value array, as demonstrated in the following example. By using valueConsistsOf
prop you can change that behavior. This prop has four options:
"ALL"
- Any node that is checked will be included in thevalue
array"BRANCH_PRIORITY"
(default) - If a branch node is checked, all its descendants will be excluded in thevalue
array"LEAF_PRIORITY"
- If a branch node is checked, this node itself and its branch descendants will be excluded from thevalue
array but its leaf descendants will be included"ALL_WITH_INDETERMINATE"
- Any node that is checked will be included in thevalue
array, plus indeterminate nodes
<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 👩',
} ],
} ],
}),
}
Disable Branch Nodes
Set disableBranchNodes: true
to make branch nodes uncheckable and treat them as collapsible folders only. You may also want to show a count next to the label of each branch node by setting 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,
}),
}
Flatten Search Results
Set flattenSearchResults: true
to flatten the tree when searching. With this option set to true
, only the results that match will be shown. With this set to false
(default), its ancestors will also be displayed, even if they would not individually be included in the results.
<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,
}),
}
Disable Item Selection
You can disable item selection by setting isDisabled: true
on any leaf node or branch node. For non-flat mode, setting on a branch node will disable all its descendants as well.
<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' ],
}),
}
Nested Search
Sometimes we need the possibility to search options within a specific branch. For example your branches are different restaurants and the leafs are the foods they order. To search for the salad order of "McDonals" restaurant, just search for "mc salad". You can also try searching "salad" to feel the difference.
Concretely speaking, your search query gets split on spaces. If each splitted string is found within the path to the node, then we have a match.
<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',
} ],
} ],
}),
}
Fuzzy matching functionality is disabled for this mode to avoid mismatching.
Customize Key Names
If your data of options is loaded via AJAX and have a different data structure than what vue-treeselect asks, e.g. your data has name
property but vue-treeselect needs label
, you may want to customize the key names. In this case, you can provide a function prop called normalizer
which will be passed every node in the tree during data initialization. Use this function to create and return the transformed object.
<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,
}
},
}),
}
Customize Option Label
You can customize the label of each option. vue-treeselect utilizes Vue's scoped slot feature and provides some props you should use in your customized template:
node
- a normalized node object (note that, this is differnt from what you return fromnormalizer()
prop)count
&shouldShowCount
- the count number and a boolean indicates whether the count should be displayedlabelClassName
&countClassName
- CSS classnames for making the style correct
<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),
}),
}
Customize Value Label
You can customize the label of value item (each item in case of multi-select). vue-treeselect utilizes Vue's scoped slot feature and provides some props you should use in your customized template:
node
- a normalized node object (note that, this is differnt from what you return fromnormalizer()
prop)
<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 Support
In all previous examples, we used v-model
to sync value between application state and vue-treeselect, a.k.a two-way data binding. If you are using Vuex, we could make use of :value
and @input
, since v-model
is just a syntax sugar for them in Vue 2.
Concretely speaking, we can bind getter
to :value
to make vue-treeselect reflect any changes in your Vuex state, and bind action
or mutation
to @input
to update your Vuex state whenever the value changes.
<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' ]),
},
}