Многоразовый универсальный компонент автозаполнения для Blazor.
В одной из последних статей я рассмотрел создание простого компонента автозаполнения в приложении Blazor WebAssembly, который динамически извлекает данные из базы данных через API в зависимости от того, что пользователь вводит на входе. Он работает хорошо, но он не предназначен для повторного использования. В этой статье я рассмотрю шаги, необходимые для преобразования компонента, чтобы его можно было подключить в любом месте приложения и работать с любыми данными.
Во-первых, я рассмотрю функциональность компонента, который я представил в прошлой статье.
- Компонент имеет элемент управления вводом
- Обработчик события
HandleInput
связан с входным событием элемента управления. - По мере того, как пользователь вводит текст, выполняется обработчик событий.
- Обработчик проверяет длину входного значения. Если длина превышает 2, выполняется запрос к API, который возвращает всех клиентов, имена которых включают входное значение.
- Клиенты, возвращенные API, отображаются в неупорядоченном списке, расположенном сразу под входными данными.
- Если пользователь выбирает клиента, щелкнув его,
SelectItem
срабатывает метод, подключенный к событию щелчка элемента списка, . - Имя выбранного клиента присваивается входным данным, а его имя и значение ключа отображаются как часть подтверждающего сообщения.
- Существующие данные из API аннулируются, а неупорядоченный список удаляется из пользовательского интерфейса.
Теперь я сосредоточусь на факторах, препятствующих повторному использованию существующего компонента в других частях приложения. Давайте рассмотрим раздел кода, где определяются данные и поведение компонента:
@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;
}
}
Основная проблема, которая потенциально препятствует использованию этого компонента в другом месте приложения, заключается в его зависимости от одного конкретного типа данных — Customer
. Если мы хотим предоставить службу автозаполнения, которая работает Product
, например, с типом данных, нам нужно ее переписать. Компонент также зависит от конкретной жестко заданной конечной точки API. В идеале мы хотим, чтобы вызывающий компонент передал эти две части данных. Конечная точка API обрабатывается просто — в конце концов, это всего лишь строка. Сначала я создам компонент Razor в файле с именем AutocompleteComponent.razor , а затем добавлю параметр в блок кода для обработки URL-адреса API. Я также добавил EditorRequired
атрибут, чтобы среда IDE выдавала предупреждение, если для параметра не указано значение:
[Parameter, EditorRequired] public string? ApiUrl { get; set; }
Тип данных - другое дело.
Создавая компонент автозаполнения, мы понятия не имеем, с какими типами данных захотят работать потребители. Нам нужно разместить любой тип данных. Для этого мы используем поддержку компонентов универсального типа Razor . По сути, параметр универсального типа действует как заполнитель для типа, который передается вызывающим компонентом. Параметр универсального типа добавляется в верхнюю часть компонента Razor с помощью typeparam
директивы:
@typeparam TItem
Компонент автозаполнения будет отвечать за создание фактических данных в ответ на ввод данных пользователем в элемент управления формы, но нам также необходимо иметь возможность манипулировать этими данными извне компонента. Например, в текущем компоненте мы устанавливаем данные, null
чтобы очистить список параметров из пользовательского интерфейса. Итак, нам нужно добавить контейнер для данных, но нам также нужно сделать его публичным свойством и добавить Parameter
к нему атрибут:
[Parameter, EditorRequired] public IEnumerable<TItem>? Items{ get; set; }
Давайте взглянем на фрагмент разметки в исходном компоненте. Он отображает детали выбранного элемента, который в данном случае является покупателем:
@if (!string.IsNullOrWhiteSpace(selectedCustomerName))
{
<p class="mt-3">
Selected customer is @selectedCustomerName with ID <strong>@selectedCustomerId</strong>
</p>
}
Теперь, когда наш компонент является общим, такие ссылки, как «Выбранный клиент», больше не имеют смысла. Во время разработки мы не знаем, что потребитель может захотеть отобразить в пользовательском интерфейсе в случае выбора элемента, если вообще что-либо, поэтому мы оставим это на усмотрение потребителя, добавив еще один параметр в компонент, тип которого будет RenderFragment
- представляющий фрагмент кода Razor для обработки и рендеринга:
[Parameter] public RenderFragment? ResultsTemplate { get; set; }
Вот еще один фрагмент разметки исходного компонента. Он отвечает за рендеринг данных, возвращаемых API в виде опций, и за добавление обработчика событий для события клика каждой опции:
@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>
}
У нас уже есть замена для customers
в новом компоненте — Items
. Таким образом, мы можем довольно легко заменить большую часть этого кода:
@if (Items is not null)
{
<ul class="options">
@if (Items.Any())
{
@foreach (var item in Items)
{
@* TODO: Render Options *@
}
}
else
{
<li class="disabled option">No results</li>
}
</ul>
}
Нам все еще нужно решить, как отображать элементы в качестве параметров и как реагировать на событие щелчка параметра. В исходном компоненте реализация обеих этих задач зависит от типа, а тип известен только вызывающему компоненту, поэтому мы переложим обе задачи на вызывающий. Во-первых, мы позволим вызывающему компоненту указать, как отображается каждый параметр, поскольку он знает о свойствах типа данных параметра, а компонент автозаполнения — нет. Поэтому мы предоставим еще один RenderFragment
параметр, за исключением того, что на этот раз мы будем использовать версию с общим типом, которая принимает параметр:
[Parameter, EditorRequired] public RenderFragment<TItem> OptionTemplate{ get; set; } = default!;
Далее мы займемся обработчиком события клика, который в настоящее время назначен li
элементу, содержащему каждую опцию. Если вы хотите, чтобы вызывающий компонент уведомлялся о событиях, происходящих в дочернем компоненте, вы делаете это с помощью EventCallback
параметра, который представляет делегат, вызываемый в родительском компоненте. Мы будем использовать строго типизированный EventCallback<TValue>
, который позволяет вызывающему компоненту указать тип данных, которые будут переданы в функцию обратного вызова родительского компонента. Мы добавляем EventCallback<TValue>
параметр с именем OnSelectItem
:
[Parameter, EditorRequired] public EventCallback<TItem> OnSelectItem { get; set; }
Нам нужен способ вызвать OnSelectItem
обратный вызов события. Мы добавляем метод к компоненту, который принимает в TItem
качестве параметра и передает его InvokeAsync
методу обратного вызова:
async Task SelectItem(TItem item) => await OnSelectItem.InvokeAsync(item);
Теперь код, отображающий каждую опцию, можно вставить в foreach
цикл, который еще предстоит завершить:
@foreach (var item in Items)
{
<li class="option" @onclick="_ => SelectItem(item)">
@OptionTemplate(item)
</li>
}
Мы почти на месте. В пункте 7 существующего функционала устанавливаем в качестве входных данных название выбранного товара. Мы позволим вызывающему компоненту установить это значение — если он захочет, предоставив параметр и привязав его к входным данным:
[Parameter] public string? SelectedValue { get; set; }
<input @bind=SelectedValue @oninput=HandleInput class="form-control filter" />
Наконец, мы реорганизуем HandleInput
обработчик события клика, чтобы он работал с ApiUrl
параметром и общими данными:
async Task HandleInput(ChangeEventArgs e)
{
filter = e.Value?.ToString();
if (filter?.Length > 2)
{
Items = await http.GetFromJsonAsync<IEnumerable<TItem>>($"{ApiUrl}{filter}");
}
else
{
Items = null;
}
}
Использование компонента
Теперь, когда мы создали компонент, мы можем использовать его в другом компоненте. В загружаемом файле, сопровождающем эту статью, я использовал его дважды на одной и той же странице — один раз для получения списка клиентов, а второй — для получения списка продуктов.
Здесь я покажу только шаги, необходимые для работы с данными клиентов. Во-первых, вот блок кода в вызывающем компоненте. Он определяет поля для данных клиента и выбранного клиента. Он также включает метод с именем SelectCustomer
, который будет передан как делегат к EventCallback
параметру:
@code {
List<Customer>? Customers;
Customer? SelectedCustomer;
void SelectCustomer(Customer customer)
{
SelectedCustomer = customer;
Customers = null;
}
}
Вот открывающий тег для компонента. Мы передаем значения для Items
параметров SelectedValue
и ApiUrl
. Мы также передаем имя SelectCustomer
метода в качестве делегата к OnSelectItem
параметру. И мы передаем параметр, Customer
чтобы TItem
компонент автозаполнения знал, какой тип передать обратно в обратный вызов события:
<AutocompleteComponent Items="Customers"
SelectedValue="@(SelectedCustomer?.CompanyName)"
OnSelectItem="SelectCustomer"
TItem="Customer"
Context="customer"
ApiUrl="/api/companyfilter?filter=">
Было передано еще одно значение; Context
Параметру был присвоен "клиент" . Это устанавливает имя выражения, используемого в строго типизированных RenderFragment
параметрах. У нас есть один из них — OptionTemplate
параметр, который добавляется внутри открывающего и закрывающего AutocompleteComponent
тегов:
<OptionTemplate>
<span class="option-text">@customer.CompanyName</span>
</OptionTemplate>
Мы также можем предоставить содержимое для ResultsTemplate
параметра перед закрывающим AutocompleteComponent
тегом, если хотим:
<ResultsTemplate>
@if (SelectedCustomer != null)
{
<p class="mt-3">
Selected customer is <strong>@SelectedCustomer.CompanyName</strong>
with ID <strong>@SelectedCustomer.CustomerId</strong>
</p>
}
</ResultsTemplate>
</AutocompleteComponent>
Резюме
Если мне нужно использовать функцию автозаполнения в нескольких местах в приложении Blazor, мне больше не нужно копировать и вставлять один и тот же код. Я могу централизовать его в одном повторно используемом и более удобном в сопровождении компоненте и делегировать решения по данным и некоторым аспектам поведения вызывающим компонентам с использованием @typeparam
строго типизированных EventCallback
и RenderFragment
параметров.
Только полноправные пользователи могут оставлять комментарии. Аутентифицируйтесь пожалуйста, используя сервисы.