Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Custom Dropdown with A-Z Index Navigation in Vue 2 Ant Design Select

Tech 2

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}`)
  }
}
Tags: Vue 2

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.