Skip to content

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-vue3

You 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

NameTypeDefaultDescription
directionStringltrSwitch to rtl to invert the tree layout.
multipleBooleantrueAllow more than one selected node.
checkboxBooleanfalseEnable checkbox mode and emit checked/unchecked events.
checkOnSelectBooleanfalseIn checkbox mode, toggle the checkbox when a node is clicked.
autoCheckChildrenBooleantrueCascade check state to descendants.
autoDisableChildrenBooleantrueCascade disabled state to descendants.
checkDisabledChildrenBooleantrueWhen false, disabled children are ignored during cascaded checks.
parentSelectBooleanfalseClicking a node label toggles expansion when it has children.
keyboardNavigationBooleantrueRegister key handlers for arrow navigation, space/enter check, and delete.
nodeIndentNumber24Horizontal indent (in px) for each nested level.
minFetchDelayNumber0Minimum time (ms) loader stays visible for async fetches.
fetchDataFunction / StringnullAsync data provider. String values are treated as URL templates, functions must return a promise or raw data.
propertyNamesObjectnullRemap data keys when consuming custom node shapes.
modelValueanynullVue v-model binding for the current selection. Emits update:modelValue.
deletionBoolean / FunctionfalseEnable delete-key removal. Use a function for custom guards.
dndBoolean / ObjectfalseEnable drag & drop. Configure with callbacks to restrict targets.
editingBoolean / ObjectfalseConfigure inline edit behaviour.
storeObjectundefinedVuex 4 integration contract – see the Vuex section below.
filterObjectsee belowConfigure filter behaviour (emptyText, matcher, plainList, showChildren).
onFetchErrorFunctionerr => { 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:

  1. Install dependencies with npm install.
  2. Build the site with npm run pages:build.
  3. On Cloudflare Pages, set the build command to npm run pages:build and the output directory to docs/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 a Selection for matching nodes. Accepts plain objects, strings, Regexps, or callback functions.
  • tree.selected() / tree.checked() – return the current selections (as Selection). Checked selection returns null when 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 to target.
  • 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 by updater into matched node data.
  • tree.sortTree(compareFn, deep?) – sort the entire model; use tree.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.

EventPayloadDescription
tree:mountedcomponent instanceFired 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.