Implementing Custom Dropdown with A-Z Index Navigation in Vue 2 Ant Design Select
A common requirement is to enhance a select dropdown with both search functionality and an A-Z index navigation panel. This implementation uses Vue 2 with Ant Design Vue's a-select component and its dropdownRender slot.
Core Component Structure
<template>
<div>
<a-select
style="width: 100%"
placeholder="Select customer"
allow-clear
show-search
:filter-option="false"
:not-found-content="loading ? undefined : null"
:defaultActiveFirstOption="false"
:getPopupContainer="resolvePopupContainer"
@search="handleSearch"
@dropdownVisibleChange="handleDropdownToggle"
>
<!-- Custom dropdown with A-Z index -->
<div
v-if="dropdownVisible"
slot="dropdownRender"
slot-scope="menu"
>
<div class="alphabet-index">
<div
v-for="letter in alphabet"
:key="letter"
@click="scrollToLetter(letter)"
class="index-letter"
@mousedown="preventDefault"
>
{{ letter }}
</div>
</div>
<v-nodes :vnodes="menu" />
</div>
<a-select-opt-group
v-for="(group, idx) in groupedData"
:key="idx"
>
<span
slot="label"
class="group-label"
:id="'group_' + instanceId + group.key"
>{{ group.key }}</span>
<a-select-option
v-for="item in group.items"
:key="item.id"
:value="item.id"
:title="item.name"
>
<span>{{ item.name }}</span>
</a-select-option>
</a-select-opt-group>
</a-select>
</div>
</template>
Component Logic Implementation
<script>
import { debounce } from 'lodash'
export default {
props: {
instanceId: {
type: String,
default: ''
}
},
components: {
VNodes: {
functional: true,
render: (h, ctx) => ctx.props.vnodes,
}
},
data() {
return {
dropdownVisible: false,
searchQuery: '',
alphabet: [],
groupedData: [],
rawData: null,
selectedId: undefined,
loading: false,
debouncedFetch: null,
}
},
created() {
this.fetchData()
this.debouncedFetch = debounce(this.fetchData, 500)
},
methods: {
handleDropdownToggle(isOpen) {
this.dropdownVisible = isOpen
},
resolvePopupContainer(triggerNode) {
return this.modalSelect ? triggerNode.parentNode : document.body
},
handleSelectionChange(value) {
this.$emit('selection-change', value)
},
flattenGroups(groups) {
return groups.flatMap(group => group.items)
},
handleSearch(query) {
this.searchQuery = query
this.debouncedFetch()
},
fetchData() {
const params = {
name: this.searchQuery,
}
this.loading = true
this.$api.fetchCustomers(params).then(response => {
if (response.success) {
this.rawData = response.data
this.processData()
} else {
this.$message.warning(response.message)
}
this.loading = false
}).catch(() => {
this.loading = false
})
},
processData() {
const groups = []
this.alphabet = []
for (const key in this.rawData) {
this.alphabet.push(key)
groups.push({
key,
items: this.rawData[key]
})
}
// Use setTimeout to ensure DOM updates
setTimeout(() => {
this.groupedData = groups
})
},
scrollToLetter(letter) {
const targetElement = document.getElementById(
'group_' + this.instanceId + letter
)
if (targetElement) {
const dropdownMenu = document.querySelector('.ant-select-dropdown-menu')
dropdownMenu.scrollTo({
behavior: 'smooth',
top: targetElement.offsetTop - 10
})
} else {
this.$message.warning(`No customers found under ${letter}`)
}
},
preventDefault(event) {
event.preventDefault()
}
}
}
</script>
Styling for the Index Panel
.alphabet-index {
position: absolute;
z-index: 99;
right: 10px;
top: 50%;
transform: translateY(-50%);
display: flex;
flex-direction: column;
align-items: center;
}
.index-letter {
margin: 0;
cursor: pointer;
color: #1677ff;
font-weight: 500;
font-size: 12px;
line-height: 20px;
}
.group-label {
font-weight: 600;
color: rgba(0, 0, 0, 0.45);
}
Common Isssues and Solutions
When multiple instances of this component exist on the same page, clicking the index letters in one instance may cause scrolling issues in others. This occurs because dropdownRender content isn't properly destroyed between instances.
Solution 1: Conditional Rendering
Add a visibility check to the dropdown wrapper:
<div
v-if="dropdownVisible"
slot="dropdownRender"
slot-scope="menu"
>
<!-- index panel content -->
</div>
Solution 2: Scoped DOM Selection
Modify the scroll functon to target the correct dropdown instance:
scrollToLetter(letter) {
const targetId = 'group_' + this.instanceId + letter
const targetElement = document.getElementById(targetId)
if (targetElement) {
const parentContainer = targetElement.closest('.ant-select-dropdown')
const scrollContainer = parentContainer.querySelector('.ant-select-dropdown-menu')
scrollContainer.scrollTo({
behavior: 'smooth',
top: targetElement.offsetTop - 10
})
} else {
this.$message.warning(`No customers found under ${letter}`)
}
}