Skip to content

Cascader

WxCascader picks a value out of a tree, one column per level: a section inside a section, a category, a region and its towns. The tree can be handed over whole or fetched level by level as the user opens it.

Child options open on click (default)

Model: []

Child options open on hover
Any level may be picked
Levels loaded from the backend

Usage

vue
<script setup lang="ts">
import { ref } from 'vue'

const section = ref([])
const options = [
  {
    label: 'Guide',
    value: 'guide',
    children: [
      { label: 'Disciplines', value: 'disciplines' },
      { label: 'Navigation', value: 'navigation' },
    ],
  },
  { label: 'Component', value: 'component', children: [{ label: 'Input', value: 'input' }] },
]
</script>

<template>
  <wx-cascader v-model="section" :options="options" placeholder="Pick a section" clearable />
</template>

The model holds the whole path — ['guide', 'navigation'] — which is what a backend needs to know where a node sits. With :emit-path="false" it holds the last value alone, and the field still shows the full path by finding it in the tree.

Which levels may be picked

By default only a leaf can be chosen: clicking a parent opens its children. check-strictly lets any level be picked, and keeps the panel open so the user can go deeper afterwards.

vue
<template>
  <wx-cascader v-model="section" :options="options" check-strictly />
</template>

expand-trigger="hover" opens the next column on hover instead of on click.

Levels from the backend

With lazy, the cascader asks load for a level when it is opened — the root on the first open, then each node. Mark the nodes that have nothing under them with leaf: true, otherwise they keep offering to expand:

vue
<script setup lang="ts">
async function load(option) {
  const parent = option ? option.value : 'root'
  const response = await fetch(`/api/regions?parent=${parent}`)
  const rows = await response.json()
  return rows.map((row) => ({ label: row.name, value: row.id, leaf: !row.has_children }))
}
</script>

<template>
  <wx-cascader v-model="town" lazy :load="load" placeholder="Pick a town" />
</template>

A level is fetched once and kept, so walking back and forth costs no requests. While a level is in flight the node shows a spinner.

The field

show-all-levels decides whether the field reads Guide / Navigation / Side Navigation or just Side Navigation; separator is what goes between the levels. With name, the value is also posted through a hidden input — the path joined by commas — for a plain form submit.

Keyboard

The trigger opens on Enter or Space. Inside the panel and move within a column, opens the node and steps into the next column, goes back to the parent, Enter picks, Esc closes.

Props

PropTypeDefaultDescription
modelValue(string | number)[] | string | number | nullnullThe path, or the last value
optionsCascaderOption[][]{ label, value, children?, disabled?, leaf? }
expandTrigger'click' | 'hover''click'How the next column opens
checkStrictlybooleanfalseAny level may be picked
emitPathbooleantrueModel holds the whole path
showAllLevelsbooleantrueField shows the whole path
separatorstring' / 'Between the levels in the field
lazybooleanfalseFetch levels as they open
load(option, path) => CascaderOption[] | PromiseFetches one level; required with lazy
clearablebooleanfalseButton that empties the field
emptyTextstring'Nothing here'Shown for an empty level
teleportbooleantrueRender the panel in a portal
placeholderstringShown while nothing is picked
size'sm' | 'md' | 'lg''md'Control height
status'default' | 'success' | 'warning' | 'error''default'Validation state
disabledbooleanfalseDisables the control
namestringPosts the value through a hidden input
idstringgeneratedOverrides the id the label points at; WxFormItem supplies one
ariaLabelstringLabel when there is no visible one

Events: update:modelValue, change, expand (CascaderOption[]), clear, open, close.

Slot: option ({ option, level }) — replaces the label of a row.

Released under the MIT License.