Простое автозаполнение для Blazor

  • Михаил
  • 12 мин. на прочтение
  • 140
  • 24 Nov 2022
  • 24 Nov 2022

Одна из вещей, которые мне действительно нравятся в Blazor, — это то, как часто в вашем приложении легко реализовать функции, которые, если они понадобятся в серверном приложении, заставят вас протестировать свой JavaScript-фу или обратиться к стороннему компоненту. Одним из примеров является компонент автозаполнения, который извлекает оперативные данные из базы данных, соответствующие тем, которые пользователь вводит в элемент управления формы, и изменяет их по мере того, как пользователь вводит данные. В этой статье я покажу, как создать такой компонент для приложения Blazor WebAssembly и оформить его как раскрывающийся список.

 

В примере используется шаблон проекта приложения Blazor WebAssembly с выбранным параметром ASP.NET Core Hosted для создания трех проектов: клиентского, серверного и общего проекта. В примере я подключился к версии базы данных Northwind. Если вы не знаете, что это такое, найдите седовласого разработчика и спросите его. Когда я ввожу текст в элемент управления формы, база данных запрашивается, и клиенты, чьи имена содержат строку, которую я ввожу, возвращаются и отображаются в том, что выглядит как раскрывающийся список. Когда я выбираю один из предложенных вариантов, мой выбор подтверждается.

Автозаполнение

Сам элемент управления представляет собой обычный ввод текста, а результаты отображаются в ulэлементе. Они помещаются в divэлемент, для которого positionустановлено значение relative, что позволяет размещать дочерние элементы абсолютно внутри него. Это ключ к позиционированию неупорядоченного списка параметров таким образом, чтобы он создавал раскрывающийся список с элементом управления вводом. Вот CSS для компонента автозаполнения:

.autocomplete {
 position: relative;
}
.autocomplete .options {
 position: absolute;
 top: 40px;
 left: 0;
 background: white;
 width: 100%;
 padding: 0;
 z-index: 10;
 border: 1px solid #ced4da;
 border-radius: 0.5rem;
 box-shadow: 0 30px 25px 8px rgba(0, 0, 0, 0.1);
}
.autocomplete .option {
 display: block;
 padding: 0.25rem;
}
.autocomplete .option .option-text {
 padding: 0.25rem 0.5rem;
}
.autocomplete .option:hover {
 background: #1E90FF;
 color: #fff;
}
.autocomplete .option.disabled {
 background-color: lightgrey;
 cursor: not-allowed;
}
.autocomplete .option.disabled:hover {
 background: lightgrey;
 color: var(--bs-body);
}

Я добавил к параметрам закругленные углы и тень блока, чтобы они более точно имитировали внешний вид раскрывающегося списка в браузере. Синий фон, который применяется к параметрам при наведении курсора, соответствует фону, применяемому в браузере Chrome. Я также объявил disabled стиль, который будет применяться к сообщению, отображаемому в случае отсутствия совпадений. Далее я перейду к разделу кода компонента. Это содержит его данные и поведение:

@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;
   }
}

Объявлен ряд полей. Первый List<Customer> представляет собой параметры, возвращаемые сервером. Второй представляет значение ключа выбранного клиента, а третий — имя выбранного клиента. Последнее поле будет использоваться для хранения значения, введенного в элемент управления вводом.

Добавляется обработчик события named HandleInput, который будет привязан к inputсобытию элемента управления вводом. Он срабатывает для каждого символа, который вводится или удаляется из элемента управления вводом. Обработчик проверяет длину входного значения и, если оно составляет три символа или более, вызывает API, который возвращает список клиентов, назначенных customersполю. Если входное значение меньше трех символов, customersполе устанавливается в null, вместе с selectedCustomerNameи selectedCustomerId.

Другой метод SelectCustomerотвечает за назначение selectedCustomerId и на selectedCustomerNameоснове строкового параметра, представляющего значение ключа выбранного клиента. Он также отвечает за установку в customerполе значения null, что очищает все параметры.

API — это минимальная конечная точка API, зарегистрированная в файле Program.cs серверного проекта :

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

Он вызывает метод реализации ICustomerManagerинтерфейса, показанного ниже:

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();
}

Этот метод получает все компании, названия которых содержат введенный текст. Названия компаний и значение фильтра в моем примере преобразуются в нижний регистр, потому что я использую базу данных SQLite, где по умолчанию сравнение строк учитывает регистр.

Вернемся к самому компоненту автозаполнения, вот код для части пользовательского интерфейса:

@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>
}

Служба HttpClientвнедряется в компонент, чтобы HandleInputметод мог использовать ее для получения данных по мере ввода пользователем. Два блока представляют наибольший интерес. Второй из них появляется только в том случае, если было выбрано имя клиента. Он содержит контент, подтверждающий детали выбора. Первый блок — это divкласс с autocompleteпримененным к нему классом. Он содержит ввод с selectedCustomerNameпривязкой к его значению и HandleInput методу, привязанному к его @oninputатрибуту. Если какие-либо совпадающие записи клиентов возвращаются из HandleInputметода, ul.otpionsотображается, и отдельные параметры отображаются для элементов списка, к которым SelectCustomerпривязан метод .onclickобработчик. Каждый из них принимает идентификатор текущего клиента в качестве параметра. Помните, что это метод, который устанавливает selectedCustomerNameи удаляет все параметры. Если совпадающие результаты не найдены, пользователь получает соответствующее уведомление:

Исходный код этой статьи доступен по адресу https://github.com/mikebrind/Blazor-Autocomplete .

Резюме

И вот оно. Никаких сторонних библиотек, никакого JavaScript. Просто очень простое автозаполнение, написанное с использованием C# и Razor, которое использует возможности платформы Blazor для отображения и скрытия элементов в зависимости от состояния данных компонента. Это почти похоже на обман.