Skip to main content

Autocomplete

Autocomplete allows users to search a large dataset by typing, with your backend filtering and returning matching options dynamically. It is powered by the onChange event on a Select widget configured for autocomplete mode.


Widget configuration

Before writing any backend code, two properties must be enabled on the Select widget in the Form Builder:

PropertyDescription
Search BoxRenders a text input inside the dropdown so the user can type to filter options
On ChangeFires an onChange event to your backend as the user types, enabling dynamic results

Both must be turned on for autocomplete to work.


How it works

  1. The user types characters into the Select widget.
  2. The SDK fires an onChange event with widgetValue set to the current search string.
  3. Your handler queries the database and returns matching options via fieldAllowedValues.
  4. The SDK renders the results as a dropdown below the input.

Handler implementation

public async Task customerSearch_onChange(string? value)
{
string query = value ?? "";

// Search customers by name or email
var matches = await _db.Customers
.Where(c =>
c.Name.Contains(query) ||
c.Email.Contains(query))
.OrderBy(c => c.Name)
.Take(20) // limit results to keep the list manageable
.ToDictionaryAsync(c => c.Id.ToString(), c => $"{c.Name} ({c.Email})");

SetFieldAllowedValues("customerSearch", matches);
}

Returning structured results

For more control over what is displayed vs. what is stored, you can include additional context in the display label:

var matches = await _db.Products
.Where(p => p.Name.Contains(query) || p.Sku.Contains(query))
.Take(20)
.ToDictionaryAsync(
p => p.Id.ToString(),
p => $"{p.Sku} — {p.Name} (${p.Price:F2})"
);

SetFieldAllowedValues("productSearch", matches);

After the user selects a result, fire an additional onChange to populate related fields:

public async Task customerSearch_onChange(string? value)
{
if (!string.IsNullOrEmpty(value))
{
// Distinguish search typing (multiple chars) from selection (a GUID)
bool isSelection = Guid.TryParse(value, out _);

if (isSelection)
{
// User selected a customer — fill in related fields
var customer = await _db.Customers.FindAsync(Guid.Parse(value));
if (customer != null)
{
record.SetField("customerId", customer.Id.ToString());
record.SetField("customerName", customer.Name);
record.SetField("billingAddress", customer.BillingAddress);
record.SetField("creditLimit", customer.CreditLimit.ToString());
}
}
else
{
// User is still typing — return search results
var matches = await _db.Customers
.Where(c => c.Name.Contains(value))
.Take(20)
.ToDictionaryAsync(c => c.Id.ToString(), c => c.Name);

SetFieldAllowedValues("customerSearch", matches);
}
}
}

Performance considerations

  • Always use Take(N) to cap result sets — returning thousands of options will degrade UI performance.
  • Add database indexes on the columns you search (Name, Email, Sku, etc.).
  • For very large datasets, consider a full-text search index or a dedicated search service.