When designing data-heavy business applications like ERPs, invoicing software, or CRM platforms, two distinct user experience bottlenecks routinely surface:
-
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. - 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) |
- 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>
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" |
+---------------------------------+
-
Existing Customer Selected: The hidden input stores
customer_id = 104, and the text input displaysAcme 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 = NULLandcustomer_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_idacross 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);
}
}
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:
Top comments (0)