mudblazor

Can I create a MudAutoComplete MultiColumn Search - Virtualized?


Is it possible to create a Virtualized MudAutoComplete MultiColumn search. See screenshot below. I was able to implement this in Telerik and would like to recreate this in MudBlazor. Note, the binding is on a hidden field ID and the combo box is Virtualized. If Virtualization is not possible the Search can begin after 3 characters of entry.

MudAuto Multi Column

Below is how I implemented it in Telerik. Perhaps that will clarify what I am looking for.

@page "/GenSingle"
@inject SerOHRDatabase serOHRDatabase
@using Telerik.DataSource
@using Telerik.DataSource.Extensions

<div>Generator Name Search:</div>

<TelerikMultiColumnComboBox TItem="ModtblGenerator"
                            OnRead="@ReadItems"
                            TValue="int"
                            ValueField="@nameof(ModtblGenerator.Id)"
                            TextField="@nameof(ModtblGenerator.GenName)"
                            Filterable="true"
                            @bind-Value="@intSelectedGenID"
                            ItemHeight="28"
                            ListHeight="520px"
                            PageSize="25"
                            ScrollMode="@DropDownScrollMode.Virtual"
                            Width= "250px"
                            OnChange="@GetSelectedGeneratorRecord"
>
    <MultiColumnComboBoxColumns>
        <MultiColumnComboBoxColumn Field="@nameof(ModtblGenerator.GenName)"
                                   Title="Gen Name"
                                   HeaderClass="header"
                                   Class="genNameCell"
                                   Width="250px"></MultiColumnComboBoxColumn>
        <MultiColumnComboBoxColumn Field="@nameof(ModtblGenerator.GenNum)"
                                   Title="Gen Num"
                                   HeaderClass="header"
                                   Width="150px"></MultiColumnComboBoxColumn>
        <MultiColumnComboBoxColumn Field="@nameof(ModtblGenerator.Street)"
                                   Title="Street"
                                   HeaderClass="header"
                                   Width="200px"></MultiColumnComboBoxColumn>
        <MultiColumnComboBoxColumn Field="@nameof(ModtblGenerator.City)"
                                   Title="City"
                                   HeaderClass="header"
                                   Width="150px"></MultiColumnComboBoxColumn>
        <MultiColumnComboBoxColumn Field="@nameof(ModtblGenerator.Province)"
                                   Title="Province"
                                   HeaderClass="header"
                                   Width="150px"></MultiColumnComboBoxColumn>
        <MultiColumnComboBoxColumn Field="@nameof(ModtblGenerator.PostalCode)"
                                   Title="Postal Code"
                                   HeaderClass="header"
                                   Width="150px"></MultiColumnComboBoxColumn>
    </MultiColumnComboBoxColumns>
</TelerikMultiColumnComboBox>

<br />
<br />

@if (selectedGenerator != null)
{
    <ComGenerator Generator="@selectedGenerator" />
}

@code {

    List<ModtblGenerator> lstGenerators;
    int intSelectedGenID;
    ModtblGenerator selectedGenerator;
   
    private void GetSelectedGeneratorRecord()
    {
        selectedGenerator = lstGenerators.Find(x => x.Id == intSelectedGenID);
    }

    //**************************************** Combo Events
    protected async Task ReadItems(MultiColumnComboBoxReadEventArgs args)
    {
        await LoadData();
       
        var result = lstGenerators.ToDataSourceResult(args.Request);
        args.Data = result.Data;
        args.Total = result.Total;
    }

    private async Task LoadData()
    {
        if (lstGenerators == null)
        {
            lstGenerators = await serOHRDatabase.GetAllGenerators();
        }
    }
}

<style>
    .header {
        font-weight: bold;
        color: black;
    }

    .genNameCell {
        color: darkblue;
        font-weight: bolder;
    }
</style>

Solution

  • I don't think the Task.Delay is a good user experience so I spent some time finding a way around it. Here I use a javascript function to detect if there's a click on page outside of the popover to close it. I've also added a clear mechanism. Script shouldn't really be in the markup, but shown here for brevity.

    @inject IJSRuntime jsRuntime
    
    <script>
        window.registerOutsideClickHandler = (dotNetObj, popoverId) => {
            document.addEventListener('click', (event) => {
                const popoverElement = document.getElementById(popoverId);
                if (popoverElement && !popoverElement.contains(event.target)) {
                    dotNetObj.invokeMethodAsync('ClosePopover');
                }
            });
        };
    </script>
    
    <MudContainer Class="mt-8">
        <MudGrid>
            <MudItem xs="12">
                <div id="popoverWrapper" @onclick="HasFocus">
                    <MudTextField @bind-Value="SearchFor" Label="Search" Immediate="true"
                    Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.Clear" OnAdornmentClick="@ClearSearch"></MudTextField>
                    <MudPopover Open="@IsOpen"
                                AnchorOrigin="@Origin.BottomCenter" TransformOrigin="@Origin.TopCenter"
                                RelativeWidth="true" Fixed="true">
                        <MudTable T="Element" Items="@Data" Hover="true"
                                  RowClass="cursor-pointer" OnRowClick="RowClickEvent" Filter="new Func<Element, bool>(FilterElements)">
                            <HeaderContent>
                                <MudTh>Name</MudTh>
                                <MudTh>Num</MudTh>
                                <MudTh>Street</MudTh>
                                <MudTh>City</MudTh>
                                <MudTh>Province</MudTh>
                                <MudTh>PostalCode</MudTh>
                            </HeaderContent>
                            <RowTemplate>
                                <MudTd DataLabel="Name">@context.Name</MudTd>
                                <MudTd DataLabel="Num">@context.Num</MudTd>
                                <MudTd DataLabel="Street">@context.Street</MudTd>
                                <MudTd DataLabel="City">@context.City</MudTd>
                                <MudTd DataLabel="Province">@context.Province</MudTd>
                                <MudTd DataLabel="Postal Code">@context.PostalCode</MudTd>
                            </RowTemplate>
                        </MudTable>
                    </MudPopover>
                </div>
            </MudItem>
            <MudItem xs="12">
                <MudField Label="Selected Value" Variant="Variant.Text">
                    @((SearchID ?? 0) > 0 ? SearchID : "Nothing Selected")
                </MudField>
            </MudItem>
        </MudGrid>
    </MudContainer>
    
    @code {
        private string SearchFor { get; set; } = string.Empty;
        private int? SearchID { get; set; } = null;
        private bool IsOpen { get; set; } = false;
    
        protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                // Register the click listener to detect clicks outside the popover
                await jsRuntime.InvokeVoidAsync("registerOutsideClickHandler", DotNetObjectReference.Create(this), "popoverWrapper");
            }
        }
    
        [JSInvokable]
        public void ClosePopover()
        {
            if (IsOpen)
            {
                IsOpen = false;
                StateHasChanged();
            }
        }
    
        private void HasFocus()
        {
            IsOpen = true;
        }
    
        private void ClearSearch()
        {
            SearchFor = string.Empty;
            SearchID = null;
            IsOpen = false;
        }
    
        private void RowClickEvent(TableRowClickEventArgs<Element> Row)
        {
            SearchID = Row.Item.ID;
            SearchFor = Row.Item.Name;
            IsOpen = false;
        }
    
        private bool FilterElements(Element Row) => FilterFunc(Row, SearchFor);
    
        private bool FilterFunc(Element Row, string searchString)
        {
            if (string.IsNullOrWhiteSpace(searchString)) return true;
            foreach (var prop in Row.GetType().GetProperties())
            {
                if (prop.GetValue(Row)?.ToString().Contains(searchString, StringComparison.OrdinalIgnoreCase) == true)
                    return true;
            }
            return false;
        }
    
        public class Element
        {
            public int ID { get; set; }
            public string Name { get; set; }
            public string Num { get; set; }
            public string Street { get; set; }
            public string City { get; set; }
            public string Province { get; set; }
            public string PostalCode { get; set; }
        }
        public List<Element> Data { get; set; } = new List<Element> {
            new Element { ID = 1, Name = "John Doe", Num = "1234", Street = "Main St", City = "Springfield", Province = "IL", PostalCode = "62701" },
            new Element { ID = 2, Name = "Jane Smith", Num = "5678", Street = "Maple Ave", City = "Greenfield", Province = "CA", PostalCode = "93927" },
            new Element { ID = 3, Name = "Michael Brown", Num = "9101", Street = "Elm St", City = "Metropolis", Province = "NY", PostalCode = "10001" }
        };
    }