Appearance
Introduction
Liquor Tree is a Vue 3 component that renders, filters, and mutates hierarchical data with a familiar tree-view UX. It supports complex selection flows, async data sources, drag and drop, inline editing, and Vuex-driven state while staying framework-friendly for both Options API and Composition API users.
Just try it. The tree you were waiting for!
Features
- drag & drop and keyboard navigation
- mobile-friendly interactions
- granular events for any tree or node action
- flexible configuration with runtime API access
- any number of instances per page
- multi-selection and checkbox-driven state
- filtering & sorting helpers
- Vuex 4 integration hooks
Getting Started
Installation
bash
npm install liquor-tree-vue3
# or
yarn add liquor-tree-vue3You don't need to import CSS manually unless you disabled bundler CSS extraction; the plugin ships with dist/liquor-tree.css.
When using the library directly in the browser, load the Vue 3 global build first and then register the component manually:
html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/liquor-tree-vue3/dist/liquor-tree.css">
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/liquor-tree-vue3/dist/liquor-tree.umd.js"></script>
<div id="app"></div>
<script>
const { createApp } = Vue
const app = createApp({
template: `<tree :data="items" />`,
data: () => ({ items: [{ text: 'Root' }] })
})
app.component('tree', window.LiquorTree)
app.mount('#app')
</script>Basic Usage
vue
<!-- App.vue -->
<template>
<tree
ref="tree"
:data="items"
:options="options"
v-model="selected"
/>
</template>
<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue'
import 'liquor-tree-vue3/dist/liquor-tree.css'
const tree = ref(null)
const selected = ref([])
const items = [
{ text: 'Item 1' },
{ text: 'Item 2' },
{
text: 'Item 3',
children: [{ text: 'Item 3.1' }, { text: 'Item 3.2' }]
}
]
const options = {
checkbox: true,
multiple: true
}
let offSelected = () => {}
onMounted(() => {
offSelected = tree.value.tree.on('node:selected', () => {
console.log('Selected nodes', [...tree.value.tree.selected()])
})
})
onBeforeUnmount(() => {
offSelected()
})
</script>Register the component globally via the plugin helper:
js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import LiquorTree from 'liquor-tree-vue3'
import 'liquor-tree-vue3/dist/liquor-tree.css'
createApp(App)
.use(LiquorTree)
.mount('#app')Local registration continues to work when you import TreeRoot directly:
js
import { TreeRoot as LiquorTree } from 'liquor-tree-vue3'
export default {
components: { LiquorTree }
}Component Options
| Name | Type | Default | Description |
|---|---|---|---|
direction | String | ltr | Switch to rtl to invert the tree layout. |
multiple | Boolean | true | Allow more than one selected node. |
checkbox | Boolean | false | Enable checkbox mode and emit checked/unchecked events. |
checkOnSelect | Boolean | false | In checkbox mode, toggle the checkbox when a node is clicked. |
autoCheckChildren | Boolean | true | Cascade check state to descendants. |
autoDisableChildren | Boolean | true | Cascade disabled state to descendants. |
checkDisabledChildren | Boolean | true | When false, disabled children are ignored during cascaded checks. |
parentSelect | Boolean | false | Clicking a node label toggles expansion when it has children. |
keyboardNavigation | Boolean | true | Register key handlers for arrow navigation, space/enter check, and delete. |
nodeIndent | Number | 24 | Horizontal indent (in px) for each nested level. |
minFetchDelay | Number | 0 | Minimum time (ms) loader stays visible for async fetches. |
fetchData | Function / String | null | Async data provider. String values are treated as URL templates, functions must return a promise or raw data. |
propertyNames | Object | null | Remap data keys when consuming custom node shapes. |
modelValue | any | null | Vue v-model binding for the current selection. Emits update:modelValue. |
deletion | Boolean / Function | false | Enable delete-key removal. Use a function for custom guards. |
dnd | Boolean / Object | false | Enable drag & drop. Configure with callbacks to restrict targets. |
editing | Boolean / Object | false | Configure inline edit behaviour. |
store | Object | undefined | Vuex 4 integration contract – see the Vuex section below. |
filter | Object | see below | Configure filter behaviour (emptyText, matcher, plainList, showChildren). |
onFetchError | Function | err => { throw err } | Custom error handler for async data. |
Filter defaults:
js
{
emptyText: 'Nothing found!',
matcher(query, node) {
const regexp = new RegExp(query, 'i')
if (!regexp.test(node.text)) {
return false
}
if (node.parent && regexp.test(node.parent.text)) {
return false
}
return true
},
plainList: false,
showChildren: true
}Structure
The component accepts an array of nodes through the data prop. Each node follows the same shape:
json
{
"id": "uuid-or-number",
"text": "Node label",
"data": { "custom": "payload" },
"children": [],
"state": {
"selected": false,
"selectable": true,
"checked": false,
"expanded": false,
"disabled": false,
"visible": true,
"indeterminate": false,
"draggable": true,
"dropable": true,
"editable": false
}
}id is optional – a UUID is generated when omitted. text is duplicated onto data.text for backwards compatibility with legacy APIs.
Guides
Basic Features
The tree exposes a runtime API that mirrors key Vue events. The component instance is accessible via ref, and the underlying tree helper provides selection, insert/remove, filtering, or navigation utilities.
js
const api = tree.value.tree
api.select(api.find({ text: 'Item 1' }))
api.expandAll()
api.collapseAll()Selections are returned as Selection objects (an Array subclass). Spread or iterate them in Composition API code:
js
const selectedNodes = [...api.selected()]
const first = api.selected().at(0)Checkboxes
Enable checkbox mode with options.checkbox = true. Use checkOnSelect to sync label clicks and autoCheckChildren / checkDisabledChildren to control cascades.
Checkbox aggregations are exposed via the selection API:
js
const checkedNodes = [...api.checked()]The following events fire during checkbox interactions: node:checked, node:unchecked, and node:editing:stop (when editing shortcuts run while a node is checked).
Redefine Structure
Keep your server payload intact by remapping property names. Provide the propertyNames option and Liquor Tree will read from the mapped keys while maintaining the default runtime shape.
js
const options = {
propertyNames: {
id: 'uuid',
text: 'label',
children: 'items',
state: 'flags'
}
}Keyboard Navigation
Arrow keys move focus between nodes. Space / Enter toggle checkboxes, Delete honours the deletion option, and Esc exits inline editing. Disable the feature by setting keyboardNavigation: false.
Filtering
Call tree.filter(query) to highlight matches, or tree.clearFilter() to reset. Matches are exposed through tree.value.matches. Customize highlighting with the filter.matcher hook – return true to mark a node as matched. When plainList is enabled, matches are rendered as a flat list; use showChildren to include or hide descendants.
Async Data
Provide options.fetchData to fetch children on demand. The helper receives the target node (with { id: 'root', name: 'root' } for initial loads) and must return either a promise or raw data:
js
const options = {
fetchData(node) {
if (node.id === 'root') {
return apiClient.get('/nodes/root')
}
return apiClient.get(`/nodes/${node.id}/children`)
},
minFetchDelay: 150
}String values are treated as URL templates: fetchData: '/api/nodes/{id}/children'. Use onFetchError to centralize error handling.
Inline Editing
Set options.editing = true to allow double-click edits. Provide callbacks to integrate with your backend:
js
const options = {
editing: {
trigger: 'doubleclick',
confirm(node, newText) {
return apiClient.patch(`/nodes/${node.id}`, { text: newText })
}
}
}Editing triggers node:editing:start and node:editing:stop events. Return false from confirm to cancel the update.
Integration with Vuex
Liquor Tree can consume Vuex 4 stores when you pass the following contract:
js
const options = {
store: {
store: vuexStore,
getter: () => vuexStore.getters['tree/items'],
dispatcher(action, payload) {
vuexStore.dispatch(action, payload)
},
mutations: ['tree/SET_ITEMS']
}
}getter must return the tree data. When mutations is omitted, every action will trigger a refresh; otherwise only listed mutation types do. Liquor Tree dispatches actions through dispatcher during drag and drop or inline editing operations so the store stays in sync.
Drag & Drop
Enable drag and drop via options.dnd. Supply guards to restrict moves:
js
const options = {
dnd: {
isDraggable(node) {
return node.state('draggable') !== false
},
isDropable(node, dragged) {
return node !== dragged && node.state('dropable') !== false
}
}
}Listen for node:dragging:start and node:dragging:finish to persist changes. Finish events include the drop target and drop position (drag-on, drag-above, or drag-below).
Deployment
Deploying to Cloudflare Pages builds the VitePress docs as a standalone site:
- Install dependencies with
npm install. - Build the site with
npm run pages:build. - On Cloudflare Pages, set the build command to
npm run pages:buildand the output directory todocs/site/.vitepress/dist.
The generated output contains only the documentation, ready for deployment without the demo pages.
The repository includes a wrangler.toml configured for Pages, enabling wrangler pages deploy or wrangler pages dev workflows without extra setup.
API
Tree API
tree refers to the runtime helper exposed as this.tree (Options API) or ref.value.tree (Composition API).
tree.on(name, handler)/tree.off(name, handler)/tree.once(name, handler)– manage event listeners.tree.find(criteria, multiple?)– return aSelectionfor matching nodes. Accepts plain objects, strings, Regexps, or callback functions.tree.selected()/tree.checked()– return the current selections (asSelection). Checked selection returnsnullwhen the checkbox mode is disabled.tree.expandAll()/tree.collapseAll()– toggle all nodes.tree.filter(query)/tree.clearFilter()– apply or reset filter state.tree.append(target, node)/tree.prepend(target, node)– insert nodes relative totarget.tree.before(target, node)/tree.after(target, node)– insert before or after.tree.remove(criteria, multiple?)/tree.removeNode(node)– remove nodes.tree.updateData(criteria, updater)– merge the object returned byupdaterinto matched node data.tree.sortTree(compareFn, deep?)– sort the entire model; usetree.sort(criteria, compareFn, deep?)to sort subtrees.tree.loadChildren(node)– trigger lazy loading for batch nodes.tree.destroy()– clean up listeners (called automatically on component unmount).
Selection API
Selections extend Array, so all array helpers are available. Additional helpers call through to the node API and return the selection for chaining:
selection.select(extend?),selection.unselect()selection.check(),selection.uncheck()selection.expand(),selection.collapse()selection.enable(),selection.disable()selection.remove()– remove every node in the selection
Spread selections or convert them to arrays when using them outside chaining contexts:
js
const nodes = [...tree.selected()]
nodes.forEach(node => console.log(node.text))Node API
Nodes represent the runtime tree data. Key helpers include:
node.parent/node.children/node.depth– traversal helpers.node.select(extend?),node.unselect()node.check(),node.uncheck(),node.indeterminate()node.expand(),node.collapse(),node.toggleExpand()node.enable(),node.disable(),node.disabled()node.show(),node.hide(),node.visible()node.append(nodeLike),node.prepend(nodeLike),node.before(nodeLike),node.after(nodeLike)node.remove(),node.empty()node.setData(data)– merge new key/value pairs into the node’s data bag.node.recurseUp(fn),node.recurseDown(fn)node.startEditing()/node.stopEditing(text?)node.startDragging()/node.finishDragging(destination, position)
node.$emit(event, ...args) dispatches through the same internal emitter as tree.on.
Events
Subscribe via tree.on('event:name', handler) or listen on the component (<tree @node:selected="onSelect" />). Every event also re-emits through the Vue component instance.
| Event | Payload | Description |
|---|---|---|
tree:mounted | component instance | Fired after the tree renders and initial data is processed. |
tree:filtered | (matches: Selection, query: string) | Emitted after tree.filter() updates matches. |
tree:data:fetch | (node) | Fired before a lazy fetch begins. |
tree:data:received | (node) | Fired after async children resolve. |
node:added | (node) | Node inserted into the model (append/prepend/before/after/addToModel). |
node:removed | (node) | Node removed from the model. |
node:selected / node:unselected | (node) | Selection state changed. |
node:checked / node:unchecked | (node, triggerNode?) | Checkbox state toggled. triggerNode is provided when the change was cascaded. |
node:expanded / node:collapsed | (node, parent?) | Node expansion state changed. Parent is passed when bubbling during expandTop. |
node:shown / node:hidden | (node) | Visibility changed due to filtering or manual toggles. |
node:enabled / node:disabled | (node) | Disabled state changed. |
node:dragging:start | (node) | Drag interaction started. |
node:dragging:finish | (node, destination, position) | Drag ended; destination and relative drop position included. |
node:text:changed | (node, newText, oldText) | Node text updated. |
node:data:changed | (node, data) | Node data merged via setData or updateData. |
node:editing:start / node:editing:stop | (node, previousText?) | Inline editing lifecycle. |
node:clicked / node:dblclick | (node) | Emitted from the rendered node content area. |
Every event emits through the tree emitter first and then bubbles through the Vue component, so you may either subscribe imperatively (tree.on) or use standard Vue listeners.