UITreeDropdown
UITreeDropdown
A versatile tree dropdown component that allows users to navigate and select items from hierarchical data structures with support for multiple selection, filtering, and customization.
<template> <UITreeDropdown v-model="selectedItems" :options="treeData" :multiple="true" :filterable="true" placeholder="Select categories" @change="handleSelectionChange" > <template #trigger="{ selectedLabels }"> <UIButton type="default"> {{ selectedLabels.length ? selectedLabels.join(', ') : 'Select' }} <ChevronDownIcon class="w-5 h-5 ml-2" /> </UIButton> </template> </UITreeDropdown></template>
<script setup lang="ts">interface TreeNode { id: string label: string children?: TreeNode[] disabled?: boolean}
const selectedItems = ref<string[]>([])
const treeData: TreeNode[] = [ { id: '1', label: 'Electronics', children: [ { id: '1-1', label: 'Phones', children: [ { id: '1-1-1', label: 'iPhone' }, { id: '1-1-2', label: 'Android' } ] }, { id: '1-2', label: 'Laptops' } ] }, { id: '2', label: 'Clothing', children: [ { id: '2-1', label: 'Men' }, { id: '2-2', label: 'Women' } ] }]
const handleSelectionChange = (values: string[]) => { console.log('Selected:', values)}</script>Props
modelValue(string | string[]): Selected value(s)options(array): Tree data structuremultiple(boolean): Enable multiple selectionfilterable(boolean): Enable search/filterplaceholder(string): Input placeholderdisabled(boolean): Disable componentloading(boolean): Loading statecheckStrictly(boolean): Parent/child selections are independentexpandOnSearch(boolean): Auto-expand on searchdefaultExpandAll(boolean): Expand all nodes by defaultdefaultExpandedKeys(array): Initially expanded node keysemptyText(string): Text when no datamaxTagCount(number): Max visible selected tagssize(‘sm’ | ‘md’ | ‘lg’): Component sizeplacement(string): Dropdown placement
Events
update:modelValue: Selection changedchange: Selection changedexpand: Node expanded/collapsedsearch: Search query changedclear: Selection clearedfocus: Component focusedblur: Component blurred
Slots
trigger: Custom trigger contentempty: Empty state contentprefix: Node prefix contentsuffix: Node suffix contentloading: Loading state contenttag: Selected tag content
Usage Examples
- Basic Tree Dropdown:
<template> <UITreeDropdown v-model="selected" :options="categories" placeholder="Select category" /></template>
<script setup>const selected = ref('')const categories = [ { id: '1', label: 'Category 1', children: [ { id: '1-1', label: 'Subcategory 1' }, { id: '1-2', label: 'Subcategory 2' } ] }]</script>- Advanced Multi-select Tree:
<template> <UITreeDropdown v-model="selectedDepartments" :options="departments" :multiple="true" :filterable="true" :checkStrictly="false" :maxTagCount="3" placeholder="Select departments" class="w-80" > <template #trigger="{ selectedLabels, selectedCount }"> <UIButton type="default" class="w-full justify-between" > <span class="truncate"> {{ selectedCount ? `${selectedCount} selected` : 'Select departments' }} </span> <ChevronDownIcon class="w-5 h-5 ml-2 flex-shrink-0" /> </UIButton> </template>
<template #tag="{ label, onClose }"> <UITag :closable="true" @close="onClose" > {{ label }} </UITag> </template>
<template #prefix="{ node, expanded }"> <FolderIcon v-if="node.children" class="w-5 h-5 mr-2" :class="{ 'text-primary-500': expanded }" /> <FileIcon v-else class="w-5 h-5 mr-2" /> </template> </UITreeDropdown></template>
<script setup>const selectedDepartments = ref([])const departments = [ { id: 'eng', label: 'Engineering', children: [ { id: 'frontend', label: 'Frontend', children: [ { id: 'react', label: 'React Team' }, { id: 'vue', label: 'Vue Team' } ] }, { id: 'backend', label: 'Backend', children: [ { id: 'api', label: 'API Team' }, { id: 'db', label: 'Database Team' } ] } ] }, { id: 'design', label: 'Design', children: [ { id: 'ui', label: 'UI Team' }, { id: 'ux', label: 'UX Team' } ] }]</script>- Async Loading Tree:
<template> <UITreeDropdown v-model="selectedLocation" :options="locations" :loading="loading" filterable placeholder="Select location" > <template #prefix="{ node, loading }"> <UISpinner v-if="loading" size="sm" class="mr-2" /> <template v-else> <FolderIcon v-if="node.children" class="w-5 h-5 mr-2" /> <MapPinIcon v-else class="w-5 h-5 mr-2" /> </template> </template> </UITreeDropdown></template>
<script setup>const selectedLocation = ref('')const locations = ref([])const loading = ref(false)
const loadLocations = async (parentId?: string) => { loading.value = true try { const response = await fetchLocations(parentId) if (!parentId) { locations.value = response } else { // Update nested children updateTreeNodes(locations.value, parentId, response) } } finally { loading.value = false }}
onMounted(() => { loadLocations()})</script>Best Practices
-
User Experience:
- Clear hierarchy
- Intuitive navigation
- Search functionality
- Loading states
-
Performance:
- Lazy loading
- Virtual scrolling
- Efficient updates
- Memory management
-
Accessibility:
- Keyboard navigation
- ARIA attributes
- Focus management
- Screen reader support
-
Mobile:
- Touch targets
- Responsive design
- Clear feedback
- Simplified interaction
Common Use Cases
- Category Selection:
<template> <div class="space-y-2"> <label class="text-sm font-medium"> Product Categories </label>
<UITreeDropdown v-model="selectedCategories" :options="categories" :multiple="true" :filterable="true" placeholder="Select categories" class="w-full" > <template #empty> <div class="p-4 text-center text-gray-500"> No categories found </div> </template> </UITreeDropdown>
<div v-if="selectedCategories.length" class="flex flex-wrap gap-2" > <UITag v-for="category in selectedCategoryLabels" :key="category" :closable="true" @close="removeCategory(category)" > {{ category }} </UITag> </div> </div></template>- Organization Structure:
<template> <UITreeDropdown v-model="selectedTeam" :options="organization" :filterable="true" class="w-72" > <template #prefix="{ node }"> <div class="w-6 h-6 rounded-full mr-2 flex items-center justify-center" :class="{ 'bg-primary-100 text-primary-600': node.type === 'department', 'bg-success-100 text-success-600': node.type === 'team' }" > <BuildingIcon v-if="node.type === 'department'" class="w-4 h-4" /> <UsersIcon v-else-if="node.type === 'team'" class="w-4 h-4" /> </div> </template>
<template #suffix="{ node }"> <UIBadge v-if="node.memberCount" size="sm" variant="gray" > {{ node.memberCount }} </UIBadge> </template> </UITreeDropdown></template>- File Browser:
<template> <UITreeDropdown v-model="selectedPath" :options="fileSystem" :defaultExpandAll="true" :filterable="true" placeholder="Select file location" > <template #prefix="{ node }"> <component :is="getFileIcon(node.type)" class="w-5 h-5 mr-2" :class="getFileIconColor(node.type)" /> </template>
<template #suffix="{ node }"> <span v-if="node.size" class="text-xs text-gray-500" > {{ formatFileSize(node.size) }} </span> </template> </UITreeDropdown></template>
<script setup>const getFileIcon = (type: string) => { switch (type) { case 'folder': return FolderIcon case 'image': return ImageIcon case 'document': return DocumentIcon default: return FileIcon }}
const getFileIconColor = (type: string) => { switch (type) { case 'folder': return 'text-primary-500' case 'image': return 'text-success-500' case 'document': return 'text-info-500' default: return 'text-gray-500' }}</script>Component Composition
The UITreeDropdown component works well with:
- UIButton for triggers
- UITag for selected items
- UIBadge for counts/status
- UISpinner for loading states
- UITooltip for additional information
- UIIcon for visual indicators
- UIInput for search functionality