Simple Autocomplete for Blazor

One of the things I really like about Blazor is how often it is easy to implement features in your application that, if needed in a server-side application, would have you testing your JavaScript-fu or reaching for a third party component. One example is an autocomplete component that fetches live data from a database that matches that which a user enters into a form control and changes as the user types. In this article, I'll show how to build such a component for a Blazor WebAssembly app and style it like a dropdown.

The example uses the Blazor WebAssembly app project template with the ASP.NET Core Hosted option selected to produce three projects - a client, server and shared project. In the example, I've hooked up to a version of the Northwind database. If you don't know what that is, find a grey-haired dev and ask them. As I type into a form control, the database is queried and customers whose names contain the string I am entering are returned and displayed in what looks like a dropdown control. When I select one of the suggested options, my selection is confirmed.

Autocomplete

The control itself is just an ordinary text input and the results are displayed in a ul element. They are placed in a div element that has its position set to relative, which enables child elements to be positioned absolutely within it. This is the key to positioning the unordered list of options so that it appears to create a dropdown with the input control. Here's the CSS for the autocomplete component:

.autocomplete {
  positionrelative;
}
.autocomplete .options {
  positionabsolute;
  top40px;
  left0;
  backgroundwhite;
  width100%;
  padding0;
  z-index10;
  border1px solid #ced4da;
  border-radius0.5rem;
  box-shadow0 30px 25px 8px rgba(0, 0, 0, 0.1);
}
.autocomplete .option {
  displayblock;
  padding0.25rem;
}
.autocomplete .option .option-text {
  padding0.25rem 0.5rem;
}
.autocomplete .option:hover {
  background#1E90FF;
  color#fff;
}
.autocomplete .option.disabled {
  background-colorlightgrey;
  cursornot-allowed;
}
.autocomplete .option.disabled:hover {
  backgroundlightgrey;
  colorvar(--bs-body);
}

I've added some rounded corners and box shadow to the options so that they more closely emulate the appearance of a dropdown in the browser. The blue background that is applied to options when hovered over matches that which is applied by the Chrome browser. I have also declared a disabled style, which will be applied to a message that is displayed in the event of no matches being found. Next, I'll move on to the code section of the component. This contains its data and behaviour:

@code {
    List<Customer>? customers;
    string? selectedCustomerId;
    string? selectedCustomerName;
    string? filter;
 
    async Task HandleInput(ChangeEventArgs e)
    {
        filter = e.Value?.ToString();
        if (filter?.Length > 2)
        {
            customers = await http.GetFromJsonAsync<List<Customer>>($"/api/companyfilter?filter={filter}");
        }
        else
        {
            customers = null;
            selectedCustomerName = selectedCustomerId = null;
        }
    }
 
    void SelectCustomer(string id)
    {
        selectedCustomerId = id;
        selectedCustomerName = customers!.First(c => c.CustomerId.Equals(selectedCustomerId)).CompanyName;
        customers = null;
    }
}

A number of fields are declared. The first is a List<Customer> which represents the options returned from the server. The second represents the selected customer's key value, and the third, the name of the selected customer. The last field will be used to store the value entered into the input control.

An event handler named HandleInput is added, which will be wired up to the input event of the input control. It fires for every character that is entered into or removed from the input control. The handler checks the length of the input's value and if it is three characters or more, calls an API that returns a list of customers which are assigned to the customers field. If the input value is less than three characters the customers field is set to null, along with the selectedCustomerName and selectedCustomerId.

Another method, SelectCustomer is responsible for assigning the selectedCustomerId and the selectedCustomerName based on a string parameter representing the selected customer's key value. It is also responsible for setting the customer field to null, which clears all options.

The API is a minimal API endpoint registered in the server project's Program.cs file:

app.MapGet("/api/companyfilter"async (string filter, [FromServicesICustomerManager manager=> 
    Results.Ok(await manager.GetFilteredCustomerNames(filter))
);

It calls a method on an implementation of the ICustomerManager interface which is shown next:

public class CustomerManager : ICustomerManager
{
	private readonly NorthwindContext context;
 
	public CustomerManager(NorthwindContext context=> this.context = context;
 
	public async Task<List<Customer>> GetFilteredCustomerNames(string filter=>
		await context.Customers
		.Where(c => c.CompanyName.ToLower().Contains(filter.ToLower()))
		.OrderBy(c => c)
		.ToListAsync();
}

This method gets all companies whose names include the text entered into the input. The company names and the filter value are converted to lower case in my example because I'm using a SQLite database where by default, string comparison are case-sensitive.

Back to the autocomplete component itself, here is the code for the UI part:

@page "/autocomplete"
@inject HttpClient http
 
<h3>Autocomplete Demo</h3>
 
<div class="autocomplete w-25">
    <input @bind=selectedCustomerName @oninput=HandleInput class="form-control filter" />
    @if (customers is not null)
    {
        <ul class="options">
            @if (customers.Any())
            {
                @foreach (var customer in customers)
                {
                    <li class="option" @onclick=@(_ => SelectCustomer(customer.CustomerId))>
                        <span class="option-text">@customer.CompanyName</span>
                    </li>
                }
            }
            else
            {
                <li class="disabled option">No results</li>
            }
        </ul>
    }
</div>
@if (!string.IsNullOrWhiteSpace(selectedCustomerName))
{
    <p class="mt-3">
        Selected customer is @selectedCustomerName with ID <strong>@selectedCustomerId</strong>
    </p>
}

The HttpClient service is injected into the component so that it can be used by the HandleInput method to acquire the data as the user types. Two blocks are of primary interest. The second of these only appears if a customer name has been selected. It contains content that confirm the selection details. The first block is the div with the autocomplete class applied to it. This contains an input with the selectedCustomerName bound to its value and the HandleInput method bound to its @oninput attribute. If any matching customer records are returned from the HandleInput method, the ul.otpions is rendered, and individual options are rendered to list items that have the SelectCustomer method bound to their onclick handler. Each one takes the current customer's ID as a parameter. Remember, this is the method that sets the selectedCustomerName and removes all the options. If no matching results are found, the user is informed accordingly:

The source code for this article is available at https://github.com/mikebrind/Blazor-Autocomplete.

Summary

And there you have it. No third party libraries, no JavaScript. Just a very simple autocomplete written using C# and Razor that leverages the power of the Blazor framework to show and hide elements based on the state of the component's data. This almost feels like cheating.