FE Master

Build Your Own Autocomplete Search

DifficultyMedium
Estimated time: 2-3 hours
0 done

Description

# Build Your Own Autocomplete Search Autocomplete is a crucial feature for improving search UX, but implementing it efficiently requires handling debouncing, caching, keyboard navigation, and accessi...

Requirements

  • **API Integration**: Fetch suggestions from an API
  • **Debouncing**: Reduce API calls while typing
  • **Caching**: Store previous results for faster responses
  • **Keyboard Navigation**: Full keyboard support (arrows, enter, escape)
  • **Highlighting**: Highlight matching text in results
  • **Loading States**: Show loading indicators
  • **Error Handling**: Gracefully handle API failures
  • **Accessibility**: Full ARIA support for screen readers

Overview

Autocomplete is a crucial feature for improving search UX, but implementing it efficiently requires handling debouncing, caching, keyboard navigation, and accessibility. This challenge will guide you through building a production-ready autocomplete system.

Getting Started

## Step Zero: Set Up Your Environment - **Create HTML**: Build a search input with results container - **Mock API**: Set up a simple API endpoint or mock data

Implementation Guide

Build Your Own Autocomplete Search

Autocomplete is a crucial feature for improving search UX, but implementing it efficiently requires handling debouncing, caching, keyboard navigation, and accessibility. This challenge will guide you through building a production-ready autocomplete system.

The Challenge - Building an Autocomplete Search

The functional requirements for your autocomplete system include:

  • API Integration: Fetch suggestions from an API
  • Debouncing: Reduce API calls while typing
  • Caching: Store previous results for faster responses
  • Keyboard Navigation: Full keyboard support (arrows, enter, escape)
  • Highlighting: Highlight matching text in results
  • Loading States: Show loading indicators
  • Error Handling: Gracefully handle API failures
  • Accessibility: Full ARIA support for screen readers

Step Zero: Set Up Your Environment

  • Create HTML: Build a search input with results container
  • Mock API: Set up a simple API endpoint or mock data

Step One: Create the Core Autocomplete Class

Example:

class Autocomplete {
    constructor(input, options = {}) {
        this.input = input;
        this.options = {
            minChars: 2,
            debounceTime: 300,
            maxResults: 10,
            apiUrl: '/api/search',
            cacheResults: true,
            ...options
        };
        
        this.cache = new Map();
        this.currentResults = [];
        this.selectedIndex = -1;
        this.isLoading = false;
        
        this.init();
    }
    
    init() {
        this.createResultsContainer();
        this.attachEvents();
    }
    
    createResultsContainer() {
        this.results = document.createElement('div');
        this.results.className = 'autocomplete-results';
        this.results.setAttribute('role', 'listbox');
        this.results.style.display = 'none';
        
        this.input.parentNode.appendChild(this.results);
    }
}

Step Two: Implement Debounced Search

Example:

attachEvents() {
    let debounceTimer;
    
    this.input.addEventListener('input', (e) => {
        const query = e.target.value.trim();
        
        clearTimeout(debounceTimer);
        
        if (query.length < this.options.minChars) {
            this.hideResults();
            return;
        }
        
        debounceTimer = setTimeout(() => {
            this.search(query);
        }, this.options.debounceTime);
    });
    
    this.input.addEventListener('keydown', (e) => {
        this.handleKeyboard(e);
    });
    
    // Close on outside click
    document.addEventListener('click', (e) => {
        if (!this.input.contains(e.target) && !this.results.contains(e.target)) {
            this.hideResults();
        }
    });
}

Step Three: Implement Search with Caching

Example:

async search(query) {
    // Check cache first
    if (this.options.cacheResults && this.cache.has(query)) {
        this.displayResults(this.cache.get(query), query);
        return;
    }
    
    this.showLoading();
    
    try {
        const response = await fetch(
            `${this.options.apiUrl}?q=${encodeURIComponent(query)}&limit=${this.options.maxResults}`
        );
        
        if (!response.ok) throw new Error('Search failed');
        
        const data = await response.json();
        
        // Cache results
        if (this.options.cacheResults) {
            this.cache.set(query, data);
        }
        
        this.displayResults(data, query);
        
    } catch (error) {
        this.showError(error.message);
    } finally {
        this.hideLoading();
    }
}

showLoading() {
    this.isLoading = true;
    this.results.innerHTML = '<div class="autocomplete-loading">Loading...</div>';
    this.results.style.display = 'block';
}

hideLoading() {
    this.isLoading = false;
}

showError(message) {
    this.results.innerHTML = `<div class="autocomplete-error">${message}</div>`;
    this.results.style.display = 'block';
}

Step Four: Display Results with Highlighting

Example:

displayResults(data, query) {
    this.currentResults = data;
    this.selectedIndex = -1;
    
    if (data.length === 0) {
        this.results.innerHTML = '<div class="autocomplete-no-results">No results found</div>';
        this.results.style.display = 'block';
        return;
    }
    
    this.results.innerHTML = '';
    
    data.forEach((item, index) => {
        const resultItem = document.createElement('div');
        resultItem.className = 'autocomplete-item';
        resultItem.setAttribute('role', 'option');
        resultItem.setAttribute('id', `autocomplete-item-${index}`);
        
        // Highlight matching text
        const highlightedText = this.highlightMatch(item.text, query);
        resultItem.innerHTML = highlightedText;
        
        resultItem.addEventListener('click', () => {
            this.selectResult(item);
        });
        
        resultItem.addEventListener('mouseenter', () => {
            this.setSelected(index);
        });
        
        this.results.appendChild(resultItem);
    });
    
    this.results.style.display = 'block';
    
    // Update ARIA attributes
    this.input.setAttribute('aria-expanded', 'true');
    this.input.setAttribute('aria-owns', 'autocomplete-results');
    this.input.setAttribute('aria-activedescendant', '');
}

highlightMatch(text, query) {
    const regex = new RegExp(`(${query})`, 'gi');
    return text.replace(regex, '<mark>$1</mark>');
}

Step Five: Implement Keyboard Navigation

Example:

handleKeyboard(e) {
    if (!this.results.style.display === 'block') return;
    
    const itemCount = this.currentResults.length;
    
    switch (e.key) {
        case 'ArrowDown':
            e.preventDefault();
            this.setSelected(
                this.selectedIndex < itemCount - 1 ? this.selectedIndex + 1 : 0
            );
            break;
            
        case 'ArrowUp':
            e.preventDefault();
            this.setSelected(
                this.selectedIndex > 0 ? this.selectedIndex - 1 : itemCount - 1
            );
            break;
            
        case 'Enter':
            e.preventDefault();
            if (this.selectedIndex >= 0) {
                this.selectResult(this.currentResults[this.selectedIndex]);
            }
            break;
            
        case 'Escape':
            e.preventDefault();
            this.hideResults();
            break;
            
        case 'Tab':
            this.hideResults();
            break;
    }
}

setSelected(index) {
    // Remove previous selection
    const items = this.results.querySelectorAll('.autocomplete-item');
    items.forEach(item => item.classList.remove('selected'));
    
    this.selectedIndex = index;
    
    if (index >= 0 && index < items.length) {
        const selectedItem = items[index];
        selectedItem.classList.add('selected');
        
        // Scroll into view
        selectedItem.scrollIntoView({
            block: 'nearest',
            behavior: 'smooth'
        });
        
        // Update ARIA
        this.input.setAttribute('aria-activedescendant', `autocomplete-item-${index}`);
    }
}

selectResult(item) {
    this.input.value = item.text;
    this.hideResults();
    
    // Trigger custom event
    this.input.dispatchEvent(new CustomEvent('autocomplete:select', {
        detail: item
    }));
    
    // Optional callback
    if (this.options.onSelect) {
        this.options.onSelect(item);
    }
}

hideResults() {
    this.results.style.display = 'none';
    this.currentResults = [];
    this.selectedIndex = -1;
    
    this.input.setAttribute('aria-expanded', 'false');
    this.input.removeAttribute('aria-activedescendant');
}

Step Six: Add Advanced Caching Strategy

Example:

class LRUCache {
    constructor(maxSize = 50) {
        this.maxSize = maxSize;
        this.cache = new Map();
    }
    
    get(key) {
        if (!this.cache.has(key)) return null;
        
        // Move to end (most recently used)
        const value = this.cache.get(key);
        this.cache.delete(key);
        this.cache.set(key, value);
        
        return value;
    }
    
    set(key, value) {
        // Remove oldest if at capacity
        if (this.cache.size >= this.maxSize) {
            const firstKey = this.cache.keys().next().value;
            this.cache.delete(firstKey);
        }
        
        this.cache.set(key, value);
    }
    
    clear() {
        this.cache.clear();
    }
}

Step Seven: Support Different Result Formats

Example:

displayResults(data, query) {
    // Support grouped results
    if (data.groups) {
        data.groups.forEach(group => {
            const groupHeader = document.createElement('div');
            groupHeader.className = 'autocomplete-group-header';
            groupHeader.textContent = group.title;
            this.results.appendChild(groupHeader);
            
            group.items.forEach(item => {
                this.renderResultItem(item, query);
            });
        });
    } else {
        data.forEach(item => this.renderResultItem(item, query));
    }
}

renderResultItem(item, query) {
    const resultItem = document.createElement('div');
    resultItem.className = 'autocomplete-item';
    
    // Support custom templates
    if (this.options.renderItem) {
        resultItem.innerHTML = this.options.renderItem(item, query);
    } else {
        resultItem.innerHTML = `
            <div class="item-title">${this.highlightMatch(item.text, query)}</div>
            ${item.description ? `<div class="item-description">${item.description}</div>` : ''}
        `;
    }
    
    resultItem.addEventListener('click', () => this.selectResult(item));
    this.results.appendChild(resultItem);
}

Step Eight: Add Recent Searches

Example:

class AutocompleteWithHistory extends Autocomplete {
    constructor(input, options) {
        super(input, options);
        this.storageKey = 'autocomplete_history';
        this.maxHistory = 10;
    }
    
    loadHistory() {
        const history = localStorage.getItem(this.storageKey);
        return history ? JSON.parse(history) : [];
    }
    
    saveToHistory(item) {
        let history = this.loadHistory();
        
        // Remove duplicates
        history = history.filter(h => h.text !== item.text);
        
        // Add to beginning
        history.unshift(item);
        
        // Limit size
        history = history.slice(0, this.maxHistory);
        
        localStorage.setItem(this.storageKey, JSON.stringify(history));
    }
    
    showHistory() {
        const history = this.loadHistory();
        if (history.length === 0) return;
        
        this.displayResults(history, '');
    }
}

Step Nine: Build a Demo

Create a complete search interface with:

  • Product search with images
  • Recent searches section
  • Category grouping
  • Loading states
  • Error handling
  • Responsive design

Step Ten: Optional - Advanced Features

  • Fuzzy search matching
  • Voice input support
  • Search suggestions/corrections ("Did you mean...")
  • Analytics tracking
  • Multi-select autocomplete
  • Infinite scroll for results

The Final Step: Clean Up and Document

  • Document all options and callbacks
  • Create comprehensive examples
  • Add accessibility testing notes
  • Include performance optimization tips

Help Others by Sharing Your Solutions!

Great search experiences drive user engagement. Share your autocomplete to help others build better search interfaces!

CrackIt Dev - Master Frontend Interviews | FE Master | System Design, DSA, JavaScript