CodeBeamOrg / CodeBeam.MudBlazor.Extensions

Useful third party extension components for MudBlazor, from the contributors.
https://mudextensions.codebeam.org/
MIT License
348 stars 60 forks source link

Extended MudComboBox Item Selection Issue #383

Closed PlayerModu closed 2 weeks ago

PlayerModu commented 3 weeks ago

Hi @mckaragoz, I'm hoping you're able to help. I'm in the middle of creating, or at least trying to, a custom component as a proof of concept to mirror something that Power BI does with dropdowns.

The component below leverages the MudComboBox and some other bits but attempts to customise it heavily by supporting custom filtering, auto-expanding/collapsing during searching. multiple nesting/groups, the ability to select all within a group, and some other things. The primary issue I'm having right now is there is something weird going on with clicking individual items, it's just collapsing the parent instead of actually selecting the item? .. my hunch is it's something with event propagation, but I tried stopPropagation on the onclick but it doesn't seem to be working. Also another issue is the select all within a group is sometimes flaky, as in it sometimes works, sometimes it doesn't.

I've had to use MudComboBox due to the whole shadow thing with the other selects, but then use MudListItemExtended for the nesting to be able to auto-expand/collapse.

I can't save on the MudExtensions playground to provide a snippet but below is the code to paste in:

@using MudExtensions.Enums;
@using MudExtensions.Services;

<style>
    .mud-popover {
        max-height: 500px !important;
        height: 500px !important;
    }

    div[id^='comboboxpopover_'] {
        max-height: 500px !important;
        height: 500px !important;
    }
</style>

@* Remove MudContainer once not in Test page and is own component *@
<MudContainer>
        <MudComboBox MultiSelection="true"
                     @key="_selectKey"
                     SelectedValues="_selectedPractices"
                     T="PracticeInformation"
                     ShowCheckbox="true"
                     Clearable="true"
                     Label="Practice *"
                     OnOpen="FocusOnTextInput"
                     AnchorOrigin="Origin.BottomCenter"
                     OnClearButtonClick="ClearButtonClicked"
                     Variant="Variant.Text">
            <MudTextFieldExtended T="string"
                                  Variant="Variant.Outlined"
                                  Clearable="true"
                                  @ref="myTextField"
                                  Class="ml-5 mt-5 mr-5 mb-5"
                                  Placeholder="Search..."
                                  AutoFocus="true"
                                  ValueChanged="@(text => FilterItems(text))"
                                  Immediate="true">
                <AdornmentEnd>
                    <MudIcon Icon="@Icons.Material.Filled.Search" Color="@Color.Primary" />
                </AdornmentEnd>
            </MudTextFieldExtended>
            <MudCheckBox Class="ml-2" T=bool Label="Select All" Value="@_selectAll" ValueChanged="@(isChecked => SelectAll(isChecked))" Color="Color.Primary" />
            @foreach (var region in groups)
            {
                @if (region.Value.Any())
                {
                    <MudListItemExtended T="string" Text="@region.Key" Style="font-weight: bold !important" Expanded="@expandedRegions.Contains(region.Key)">
                        <NestedList>
                            @foreach (var area in region.Value)
                            {
                                @if (area.Value.Any())
                                {
                                    <div style="margin-left: 20px;">
                                        <MudListItemExtended T="string" Text="@area.Key" Style="font-weight: bold !important" Expanded="@expandedAreas.Contains(area.Key)">
                                            <NestedList>
                                                <MudCheckBox Class="ml-2" T=bool Label="@($"Select All In {area.Key}")" Value="@selectAllAreaFlags[region.Key][area.Key]" ValueChanged="@(isChecked => SelectAllArea(area, (bool)isChecked, region.Key, area.Key))" />
                                                @foreach (var practice in area.Value)
                                                {
                                                    <MudComboBoxItem Value="@(practice)" Text="@practice.PracticeName">@(practice.PracticeName)</MudComboBoxItem>
                                                }
                                            </NestedList>
                                        </MudListItemExtended>
                                    </div>
                                }
                            }
                        </NestedList>
                    </MudListItemExtended>
                }
            }
        </MudComboBox>
</MudContainer>

@code {
    /// <summary>
    /// Current limitiations:
    ///     - Need to add the ability to pass back _selectedValues to the caller
    ///     - Select All in an Area is flaky
    ///     - Selecting an individual item doesn't work
    /// </summary>

    // For Testing
    private IEnumerable<PracticeInformation> _userPractices;

    // Rendering
    private int _selectKey = 0;
    MudTextFieldExtended<string> myTextField;

    // User Input
    private List<PracticeInformation> _selectedPractices = new List<PracticeInformation>();
    private bool _selectAll = false;

    // Component Data
    Dictionary<string, Dictionary<string, List<PracticeInformation>>> groups;
    Dictionary<string, Dictionary<string, bool>> selectAllAreaFlags;
    private HashSet<string> expandedRegions = new HashSet<string>();
    private HashSet<string> expandedAreas = new HashSet<string>();

    protected override async Task OnInitializedAsync()
    {
        _userPractices = new List<PracticeInformation>
        {
            new PracticeInformation{PracticeName = "Practice A", AreaName = "Area 1", RegionName = "Region 1"},
            new PracticeInformation{PracticeName = "Practice B", AreaName = "Area 1", RegionName = "Region 1"},
            new PracticeInformation{PracticeName = "Practice C", AreaName = "Area 2", RegionName = "Region 1"},
            new PracticeInformation{PracticeName = "Practice D", AreaName = "Area 3", RegionName = "Region 2"},
            new PracticeInformation{PracticeName = "Practice E", AreaName = "Area 3", RegionName = "Region 2"},
            new PracticeInformation{PracticeName = "Practice F", AreaName = "Area 4", RegionName = "Region 3"},
        };

        SetGroupsToDefaultValues(_userPractices.ToList());
    }

    protected async Task FocusOnTextInput()
    {
        await myTextField.InputReference.ElementReference.FocusAsync();
    }

    private void ClearButtonClicked()
    {
        _selectedPractices = new List<PracticeInformation>();
        SetGroupsToDefaultValues(_userPractices.ToList());

        _selectKey++;
        StateHasChanged();
    }

    private void FilterItems(string searchText)
    {
        expandedRegions.Clear();
        expandedAreas.Clear();

        if (string.IsNullOrEmpty(searchText))
        {
            SetGroupsToDefaultValues(_userPractices.ToList());
        }
        else
        {
            var filteredGroups = new Dictionary<string, Dictionary<string, List<PracticeInformation>>>();
            foreach (var region in groups)
            {
                if (region.Value.Any(area => area.Value.Any(practice => practice.PracticeName.Contains(searchText))))
                {
                    expandedRegions.Add(region.Key);
                    foreach (var area in region.Value)
                    {
                        if (area.Value.Any(practice => practice.PracticeName.Contains(searchText)))
                        {
                            expandedAreas.Add(area.Key);
                        }
                    }
                }

                var filteredAreas = new Dictionary<string, List<PracticeInformation>>();
                foreach (var area in region.Value)
                {
                    filteredAreas[area.Key] = area.Value.Where(item => item.PracticeName.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToList();
                }
                filteredGroups[region.Key] = filteredAreas;
            }
            groups = filteredGroups;
        }

        StateHasChanged();
    }

    private void SelectAll(bool isChecked)
    {
        _selectedPractices.Clear();
        _selectAll = isChecked;
        if (isChecked)
        {
            foreach (var region in groups)
            {
                foreach (var area in region.Value)
                {
                    foreach (var practice in area.Value)
                    {
                        if (!_selectedPractices.Contains(practice))
                        {
                            _selectedPractices.Add(practice);
                        }
                    }
                }
            }
        }
        _selectKey++;
        StateHasChanged();
    }

    private void SelectAllArea(
            KeyValuePair<string, List<PracticeInformation>> area,
            bool isChecked,
            string regionName,
            string areaName)
    {
        bool selectAllArea = selectAllAreaFlags[regionName][areaName];
        selectAllArea = isChecked;

        if (isChecked)
        {
            foreach (var practice in area.Value)
            {
                if (!_selectedPractices.Contains(practice))
                {
                    _selectedPractices.Add(practice);
                }
            }
        }
        else
        {
            _selectedPractices.RemoveAll(practice => area.Value.Contains(practice));
        }

        // Create a new list to force the MudComboBox to update
        _selectedPractices = new List<PracticeInformation>(_selectedPractices);

        selectAllAreaFlags[regionName][areaName] = selectAllArea;
        _selectKey++;
        StateHasChanged();
    }

    private void SetGroupsToDefaultValues(List<PracticeInformation> practices)
    {
        groups = new Dictionary<string, Dictionary<string, List<PracticeInformation>>>();
        foreach (var practice in practices)
        {
            if (!groups.ContainsKey(practice.RegionName))
            {
                groups[practice.RegionName] = new Dictionary<string, List<PracticeInformation>>();
            }

            if (!groups[practice.RegionName].ContainsKey(practice.AreaName))
            {
                groups[practice.RegionName][practice.AreaName] = new List<PracticeInformation>();
            }

            groups[practice.RegionName][practice.AreaName].Add(practice);
        }

        var orderedDict = groups
            .OrderBy(kv1 => kv1.Key)
            .ToDictionary(
                kv1 => kv1.Key,
                kv1 => kv1.Value
                    .OrderBy(kv2 => kv2.Key)
                    .ToDictionary(
                        kv2 => kv2.Key,
                        kv2 => kv2.Value.OrderBy(str => str.PracticeName).ToList()
                    )
            );
        groups = orderedDict;

        selectAllAreaFlags = new Dictionary<string, Dictionary<string, bool>>();
        foreach (var practice in practices)
        {
            if (!selectAllAreaFlags.ContainsKey(practice.RegionName))
            {
                selectAllAreaFlags[practice.RegionName] = new Dictionary<string, bool>();
            }

            if (!selectAllAreaFlags[practice.RegionName].ContainsKey(practice.AreaName))
            {
                selectAllAreaFlags[practice.RegionName][practice.AreaName] = false;
            }
        }

        StateHasChanged();
    }

    public class PracticeInformation
    {
        public string PracticeName { get; set; }
        public string AreaName { get; set; }
        public string RegionName { get; set; }
    }
}
PlayerModu commented 2 weeks ago

Found a different solution for now