A Generic Dropdown control for WASM Blazor

Drop down, ye heavens, from above      הַרְעִיפוּ שָׁמַיִם מִמַּעַל

I thought that the Hebrew quote above and it's accompanying translation from the King James Bible (Isaiah 45:8) would make a good introduction to a short post on designing a dropdown control in Blazor. Truth be told, the King James translation of the first word isn't really that accurate. The Hebrew word "Harifu" translates roughly as "imparts" though it can also refer to "shower". However, since I'm talking about dropdown controls I figure we'll stick with King James' translation.

I recently needed to build a form for an application where I wanted to incorporate a dropdown control that could bind to any kind of class, and not just a string. I saw a few examples on Stack Overflow but none of them incorporated all the features that I wanted, and in a few cases, the code didn't work correctly. I thought I'd share the simple solution I came up with in case someone out there is looking for something similar.

In the past, I'd always created Blazor dropdowns using <select> and <option> elements. They work fine when you're dealing with string values but in this instance, I wanted to be able to iterate over a List of objects of some specified type and leverage a property or method to display the label for each dropdown item. Looking at the Dropdown component found in Bootstrap 5, they use a combination of <div>, <button> and/or <a> elements to produce a working widget. Since our goal is to create a pure Blazor component, we have to make some changes to the basic Bootstrap 5 design to handle mouse events and bind the selected dropdown item appropriately.

The DropDown<TItem> Razor class and its code behind below show how this accomplished. You may notice that I'm using a handler for the @onfocusout event on this control. While looking at other examples which did not include anything other than an @onmousedown handler, I noticed that the dropdown wouldn't close when you released the mouse button. Some examples tried using a @onblur event to resolve this, but I found this didn't work satisfactorily, hence the @onfocusout event show here.


@typeparam TItem
<div class="dropdown" @onfocusout="OnFocusOut">
    <button class="btn btn-primary dropdown-toggle @(Show ? "show" : string.Empty)" data-toggle="dropdown" type="button" @onmousedown="OnMouseDown" aria-haspopup="true" aria-expanded="false">
        @Label
    </button>
    <CascadingValue Value="@this">
        <div class="dropdown-menu @(Show ? "show" : string.Empty)">
            @ChildContent
        </div>
    </CascadingValue>
</div>


 public partial class DropDown<TItem>
     {
        [Parameter]
        public RenderFragment? Label { get; set; }
        [Parameter]
        public RenderFragment? ChildContent { get; set; }
        [Parameter]
        public EventCallback<TItem?> OnSelected { get; set; }
        private bool Show { get; set; } = false;
        private void OnMouseDown()
        {
            Show = !Show;
        }

        private void OnFocusOut()
        {
            Show = false;
            StateHasChanged();
        }

        public async Task HandleSelect(TItem? item)
        {
            Show = false;
            await OnSelected.InvokeAsync(item);
        }
    }

In order to display the dropdown items, I use the following code. Note that I chose to use <button> elements instead of <a> anchors. I found that anchors sometimes exhibit some odd behavior when you specify a hash character for the href attribute value.


@typeparam TItem
<button class="dropdown-item" type="button" @onmousedown="OnMouseDown">@RenderLabel</button>


public partial class DropDownItem<TItem> : ComponentBase
    {
        [CascadingParameter]
        public DropDown<TItem> DropDown { get; set; }
        [Parameter]
        public TItem Item { get; set; }
        [Parameter]
        public RenderFragment<TItem> Label { get; set; }

        private async Task OnMouseDown()
        {
            if (DropDown != null)
            {
                await DropDown.HandleSelect(Item, Label);
            }
        }

        private RenderFragment RenderLabel => Label != null && Item != null ? Label(Item) : null;
    }

A simple example of how to use this control is seen below. The type of class I use here, City, has a string property called Name that I use to label each dropdown item. You can see the relevant CallbackEvent (OnSelected) and other code referenced here in this Github repository.


<DropDown TItem="City" OnSelected="@OnSelectedCity">
    <Label>@((SelectedCity == null) ? "Select a City" : SelectedCity.Name)</Label>
		<ChildContent>
			@if (Cities != null)
				foreach (var city in Cities)
                {
					<DropDownItem TItem="City" Item="@city">
                    <Label>@city.Name</Label>
                    </DropDownItem>
                }
		</ChildContent>
</DropDown>

You can watch a short video demonstrating the use of the dropdown below.

The source code shown in this posting and a simple demo Razor page utilizing the dropdown can be found in my Github repository.

No comments:

Post a Comment