DEV Community

Blessing Chaumba
Blessing Chaumba

Posted on

Eliminating Form Context-Switching: Hash-Cached AutoSuggest with Lazy Entity Fallbacks

When designing data-heavy business applications like ERPs, invoicing software, or CRM platforms, two distinct user experience bottlenecks routinely surface:

  1. Massive Dropdown Lag: Standard HTML <select> elements with hundreds or thousands of <option> elements bloat the DOM, degrade browser memory, and create sluggish UI interactions.
  2. Context-Switching Pain (Workflow Interruption): A user is halfway through creating a quote for a new prospect. They reach the "Customer" field, open the dropdown, and realize the customer doesn't exist in the database yet. Standard validation forces them to abandon the form, navigate to the "Customers" module, create the record, return to quotes, and start over.

To solve both issues, I built a lightweight Vanilla JS AutoSuggest library centered around two core principles: Hash-Reconciliation Option Caching and Non-Blocking Lazy Entity Creation.


Concept 1: Hash-Reconciliation Caching

Instead of sending network requests for every keystroke or downloading thousands of items every time a user focuses an input, the component uses hash verification:

 Client (AutoSuggest)                      Server Backend
  |                                              |
  |--- POST { hash: "a1b2c3" } ----------------->|
  |                                              |-- Check if options modified
  |<-- 200 OK { hasUpdate: false } --------------|   since "a1b2c3"
  |   (Use cached array for client filtering)    |
Enter fullscreen mode Exit fullscreen mode
  • Upon focus, the component sends its last known dataset hash to the server.
  • If the database records haven't changed, the backend responds with a lightweight { hasUpdate: false }.
  • The component performs fast client-side array filtering (Array.prototype.filter) locally without downloading duplicate payloads.

Concept 2: Non-Blocking Lazy Entity Creation

To eliminate form abandonment during quote or invoice generation, the component decouples human-readable display values from foreign keys using a dual-input architecture:

<div class="suggest-container">
    <!-- Displays human-readable label -->
    <input type="text" name="customer_name" placeholder="Search or type customer name..." />

    <!-- Holds database Primary Key (e.g., customer_id) -->
    <input type="hidden" name="customer_id" />
</div>
Enter fullscreen mode Exit fullscreen mode

How the Fallback Mechanism Works

                     +-------------------------------+
                     | User Types Customer Name      |
                     +---------------+---------------+
                                     |
                    /----------------+\
                   /                   \
      [ Selects Existing ]       [ Types Unlisted Name ]
                 |                         |
                 v                         v
       customer_id = "104"       customer_id = ""
       customer_name = "Acme"    customer_name = "New Co"
                 |                         |
                 +------------+------------+
                              |
                              v
             +---------------------------------+
             | Save Quotation Record           |
             | - customer_id: NULL (or 104)    |
             | - customer_name: "New Co"       |
             +---------------------------------+
Enter fullscreen mode Exit fullscreen mode
  • Existing Customer Selected: The hidden input stores customer_id = 104, and the text input displays Acme Ltd.
  • New Customer Entered: The user simply types New Company Inc.. The hidden input remains empty (customer_id = ""), while the raw string is retained.
  • Database Submission: The quotation table saves customer_id = NULL and customer_name_fallback = "New Company Inc.".
  • Rendering & Lazy Linking: The quotation prints and renders normally using the fallback name. The user never had to stop mid-quote to register a customer profile. Later, when the customer profile is formally created, the backend can retroactively link customer_id across historical quotes.

Core JavaScript Implementation

Here is a simplified overview of how the component handles inputs, hashes, and loose text changes:

export class AutoSuggest {
    constructor(container, options = {}) {
        this.container = typeof container === 'string' 
            ? document.querySelector(container) 
            : container;

        this.input = this.container.querySelector('input[type="text"]');
        this.hiddenInput = this.container.querySelector('input[type="hidden"]');

        this.options = { onSelect: null, onLooseChange: null, ...options };
        this.cachedData = [];
        this.cachedHash = null;

        this.init();
    }

    async fetchOptions() {
        const endpoint = this.input.getAttribute('data-suggestion');
        if (!endpoint) return this.cachedData;

        // Post local hash to verify if server-side dataset updated
        const res = await fetch(endpoint, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ hash: this.cachedHash })
        });

        const data = await res.json();
        if (data?.hasUpdate && Array.isArray(data.data)) {
            this.cachedHash = data.hash;
            this.cachedData = data.data;
        }

        return this.cachedData;
    }

    async handleInput(query) {
        // If query differs from last selected item, clear hidden ID
        if (query !== this.selectedLabel) {
            this.hiddenInput.value = '';
            if (this.options.onLooseChange) {
                this.options.onLooseChange(query);
            }
        }

        const dataSet = await this.fetchOptions();
        const searchTerm = query.toLowerCase().trim();
        const filtered = dataSet.filter(item => item.text.toLowerCase().includes(searchTerm));

        this.renderDropdown(filtered);
    }

    select(id, label) {
        this.selectedLabel = label;
        this.input.value = label;
        this.hiddenInput.value = id; // Set foreign key
        this.hideDropdown();

        if (this.options.onSelect) this.options.onSelect(id, label);
    }
}
Enter fullscreen mode Exit fullscreen mode

Benefits Summary

  • Zero Input Blocking: Users complete forms smoothly without leaving the page.
  • Reduced Overhead: Hash reconciliation avoids re-downloading large option arrays on every keypress.
  • Clean Data Degradation: Database schema maintains explicit foreign keys (customer_id) while safely supporting fallback strings (customer_name).

GitHub Repository

Check out the source code, CSS styles, and full documentation on GitHub:

👉 https://github.com/bcngara/autosuggest-dropdown

Top comments (0)