diff --git a/BlazorAppTest/Pages/HxMultiSelectTest.razor b/BlazorAppTest/Pages/HxMultiSelectTest.razor index 70a5645d5..ec7a2664c 100644 --- a/BlazorAppTest/Pages/HxMultiSelectTest.razor +++ b/BlazorAppTest/Pages/HxMultiSelectTest.razor @@ -1,38 +1,75 @@ @page "/HxMultiSelectTest" @using System.Globalization -

HxCheckboxList

+

HxMultiSelect

+ - + + + + + + + + + + Couldn't find any matching cultures + +

Selected values: @String.Join(", ", model.CultureInfos ?? Enumerable.Empty())

@code { - private bool enabled = true; - private Model model = new Model(); - private List data; - - protected override async Task OnParametersSetAsync() - { - await base.OnParametersSetAsync(); - await Task.Delay(3000); - - data = CultureInfo.GetCultures(CultureTypes.SpecificCultures) - .OrderBy(item => item.EnglishName) - .Take(100) - .OrderByDescending(i => i.ToString()) // sorting test - .ToList(); - } - - private class Model - { - public List CultureInfos { get; set; } - } + private bool enabled = true; + private Model model = new Model(); + private List data; + + protected override async Task OnParametersSetAsync() + { + await base.OnParametersSetAsync(); + await Task.Delay(3000); + + data = CultureInfo.GetCultures(CultureTypes.SpecificCultures) + .OrderBy(item => item.EnglishName) + .Take(100) + .OrderByDescending(i => i.ToString()) // sorting test + .ToList(); + } + + private class Model + { + public List CultureInfos { get; set; } + } } \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/DemoDataService.cs b/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/DemoDataService.cs index 0b03048f4..4f85b9a92 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/DemoDataService.cs +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/DemoDataService.cs @@ -1,6 +1,4 @@ -using System.Threading; - -namespace Havit.Blazor.Components.Web.Bootstrap.Documentation.DemoData; +namespace Havit.Blazor.Components.Web.Bootstrap.Documentation.DemoData; public class DemoDataService : IDemoDataService { @@ -542,6 +540,15 @@ public IEnumerable GetAllEmployees() return employees.ToList(); } + public async Task> GetAllEmployeesAsync(CancellationToken cancellationToken = default) + { + logger.LogInformation("DemoDataService.GetAllEmployeesAsync() called."); + + await Task.Delay(150, cancellationToken); // simulate server call + + return employees.ToList(); + } + public IQueryable GetEmployeesAsQueryable() { logger.LogInformation("DemoDataService.GetEmployeesAsQueryable() called."); diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/IDemoDataService.cs b/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/IDemoDataService.cs index 7376db034..dec17d674 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/IDemoDataService.cs +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/DemoData/IDemoDataService.cs @@ -3,6 +3,7 @@ public interface IDemoDataService { IEnumerable GetAllEmployees(); + Task> GetAllEmployeesAsync(CancellationToken cancellationToken = default); IQueryable GetEmployeesAsQueryable(); Task> GetEmployeesDataFragmentAsync(int startIndex, int? count, CancellationToken cancellationToken = default); diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_DataProvider.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_DataProvider.razor index a12295ab5..228200cae 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_DataProvider.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxGridDoc/HxGrid_Demo_DataProvider.razor @@ -8,7 +8,7 @@ @code { private Task> GetGridData(GridDataProviderRequest request) { - // you usualy pass the data-request to your API/DataLayer and it returns just the few requested items (+ total count) + // you usually pass the data-request to your API/DataLayer and it returns just the few requested items (+ total count) var cultureInfos = CultureInfo.GetCultures(CultureTypes.AllCultures).ToList(); cultureInfos.Sort(request.Sorting.ToGenericPropertyComparer()); // Just a demo. NEVER use in production code! diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_BasicUsage.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_BasicUsage.razor index f4e4bc8a1..139bdac48 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_BasicUsage.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_BasicUsage.razor @@ -1,31 +1,23 @@ - + ValueSelector="@(p => p.Id)" + NullDataText="Loading employees..." + EmptyText="-select values-" /> -

Selected initials: @String.Join(' ', selectedPersonsInitials)

+

Selected employees (IDs): @String.Join(", ", selectedEmployeeIds.Select(e => e.ToString()))

@code { - private List people; - private List selectedPersonsInitials { get; set; } = new(); - - protected override async Task OnInitializedAsync() - { - await Task.Delay(1000); // simulates slow server API call - - people = new List - { - new Person("Starr Ringo", "RS"), - new Person("Lennon John", "JL"), - new Person("McCartney Paul", "PMC"), - new Person("Harrison George", "GH") - }; - } + private IEnumerable employees; + private List selectedEmployeeIds = new(); - private record Person(string Name, string Initials); + protected override async Task OnInitializedAsync() + { + employees = await DemoDataService.GetAllEmployeesAsync(); + } } \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_CustomFiltering.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_CustomFiltering.razor new file mode 100644 index 000000000..88602791a --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_CustomFiltering.razor @@ -0,0 +1,35 @@ +@inject IDemoDataService DemoDataService + + + + No employees found + + + +

Selected employees (IDs): @String.Join(", ", selectedEmployeeIds.Select(e => e.ToString()))

+ +@code { + private IEnumerable employees; + private List selectedEmployeeIds = new(); + + protected override async Task OnInitializedAsync() + { + employees = await DemoDataService.GetAllEmployeesAsync(); + } + + private bool IsRecordIncluded(EmployeeDto employee, string filterQuery) + { + return employee.Name.Contains(filterQuery, StringComparison.OrdinalIgnoreCase) + || employee.Email.Contains(filterQuery, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_Filtering.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_Filtering.razor new file mode 100644 index 000000000..b5f007aea --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_Filtering.razor @@ -0,0 +1,24 @@ +@inject IDemoDataService DemoDataService + + + +

Selected employees (IDs): @String.Join(", ", selectedEmployeeIds.Select(e => e.ToString()))

+ +@code { + private IEnumerable employees; + private List selectedEmployeeIds = new(); + + protected override async Task OnInitializedAsync() + { + employees = await DemoDataService.GetAllEmployeesAsync(); + } +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_SelectAll.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_SelectAll.razor new file mode 100644 index 000000000..6c1ad9f74 --- /dev/null +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Demo_SelectAll.razor @@ -0,0 +1,24 @@ +@inject IDemoDataService DemoDataService + + + +

Selected employees (IDs): @String.Join(", ", selectedEmployeeIds.Select(e => e.ToString()))

+ +@code { + private IEnumerable employees; + private List selectedEmployeeIds = new(); + + protected override async Task OnInitializedAsync() + { + employees = await DemoDataService.GetAllEmployeesAsync(); + } +} \ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Documentation.razor b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Documentation.razor index e66099247..e287b961e 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Documentation.razor +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/Pages/Components/HxMultiSelectDoc/HxMultiSelect_Documentation.razor @@ -1,21 +1,46 @@ @attribute [Route("/components/" + nameof(HxMultiSelect))] - + - - + + - - + +

+ You can enable filtering by setting AllowFiltering="true".
+ You can use the FilterEmptyResultTemplate to customize the message displayed when no items are found. +

+ - - Height of the dropdown menu. - + +

+ You can customize filtering by setting FilterPredicate to a delegate that returns bool and accepts two parameters: TItem and string (filter).
+ You can use the FilterEmptyResultTemplate to customize the content displayed when no items are found. +

+ - - Height of the dropdown menu. - + +

+ You can enable the "-select all-" option by setting AllowSelectAll="true".
+ You can customize the text of the "-select all-" option by setting SelectAllText. +

+ -
+
+ + + + Height of the dropdown menu. + + + + Background color of the dropdown menu. + + + + Opacity of the filter input icon. + + +
\ No newline at end of file diff --git a/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml b/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml index 6e31b54ab..3a3b20702 100644 --- a/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml +++ b/Havit.Blazor.Components.Web.Bootstrap.Documentation/XmlDoc/Havit.Blazor.Components.Web.Bootstrap.xml @@ -2701,34 +2701,17 @@ - - - Custom CSS class to render with input-group span. - - - - - Input-group at the beginning of the input. - - - - - Input-group at the beginning of the input. - - - + - Input-group at the end of the input. + Additional attributes to be splatted onto an underlying HTML element. - - - Input-group at the end of the input. - + + - + - Additional attributes to be splatted onto an underlying HTML element. + Receives notification from JavaScript when item is hidden. @@ -4384,6 +4367,52 @@ Input-group at the end of the input. + + + Enables filtering capabilities. + + + + + Defines a custom filtering predicate to apply to the list of items. + If not specified, the default behavior filters items based on whether the item text (obtained via TextSelector) contains the filter query string. + + + + + When enabled the filter will be cleared when the dropdown is closed. + + + + + Template that defines what should be rendered in case of empty items. + + + + + Text to display when the filtered results list is empty and when not using . + + + + + Enables select all capabilities. + + + + + Text to display for the select all dropdown option. + + + + + Icon displayed in filter input for searching the filter. + + + + + Icon displayed in filter input for clearing the filter. + + @@ -5021,6 +5050,31 @@ Input size. + + + Enables filtering capabilities. + + + + + Enables select all capabilities. + + + + + When enabled the filter will be cleared when the dropdown is closed. + + + + + Icon displayed in filter input for searching the filter. + + + + + Icon displayed in filter input for clearing the filter. + + Settings for . diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs index 25d5cbc37..f6a00c5e2 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs @@ -110,6 +110,56 @@ public class HxMultiSelect : HxInputBase>, IInputWit /// [Parameter] public RenderFragment InputGroupEndTemplate { get; set; } + /// + /// Enables filtering capabilities. + /// + [Parameter] public bool? AllowFiltering { get; set; } + protected bool AllowFilteringEffective => this.AllowFiltering ?? this.GetSettings()?.AllowFiltering ?? GetDefaults().AllowFiltering; + + /// + /// Defines a custom filtering predicate to apply to the list of items. + /// If not specified, the default behavior filters items based on whether the item text (obtained via TextSelector) contains the filter query string. + /// + [Parameter] public Func FilterPredicate { get; set; } + + /// + /// When enabled the filter will be cleared when the dropdown is closed. + /// + [Parameter] public bool? ClearFilterOnHide { get; set; } + protected bool ClearFilterOnHideEffective => this.ClearFilterOnHide ?? this.GetSettings()?.ClearFilterOnHide ?? GetDefaults().ClearFilterOnHide; + + /// + /// Template that defines what should be rendered in case of empty items. + /// + [Parameter] public RenderFragment FilterEmptyResultTemplate { get; set; } + + /// + /// Text to display when the filtered results list is empty and when not using . + /// + [Parameter] public string FilterEmptyResultText { get; set; } + + /// + /// Enables select all capabilities. + /// + [Parameter] public bool? AllowSelectAll { get; set; } + protected bool AllowSelectAllEffective => this.AllowSelectAll ?? this.GetSettings()?.AllowSelectAll ?? GetDefaults().AllowSelectAll; + + /// + /// Text to display for the select all dropdown option. + /// + [Parameter] public string SelectAllText { get; set; } + + /// + /// Icon displayed in filter input for searching the filter. + /// + [Parameter] public IconBase FilterSearchIcon { get; set; } + protected IconBase FilterSearchIconEffective => this.FilterSearchIcon ?? this.GetSettings()?.FilterSearchIcon ?? GetDefaults().FilterSearchIcon; + + /// + /// Icon displayed in filter input for clearing the filter. + /// + [Parameter] public IconBase FilterClearIcon { get; set; } + protected IconBase FilterClearIconEffective => this.FilterClearIcon ?? this.GetSettings()?.FilterClearIcon ?? GetDefaults().FilterClearIcon; private List itemsToRender; private HxMultiSelectInternal hxMultiSelectInternalComponent; @@ -136,17 +186,20 @@ private void RefreshState() } } - private void HandleItemSelectionChanged(bool @checked, TItem item) + private void HandleItemSelectionChanged(List itemsSelected, List itemsDeselected) { var newValue = Value == null ? new List() : new List(Value); - TValue value = SelectorHelpers.GetValue(ValueSelector, item); - if (@checked) + + foreach (var item in itemsDeselected) { - newValue.Add(value); + TValue value = SelectorHelpers.GetValue(ValueSelector, item); + newValue.Remove(value); } - else + + foreach (var item in itemsSelected) { - newValue.Remove(value); + TValue value = SelectorHelpers.GetValue(ValueSelector, item); + newValue.Add(value); } CurrentValue = newValue; // setter includes ValueChanged + NotifyFieldChanged @@ -181,13 +234,23 @@ protected override void BuildRenderInput(RenderTreeBuilder builder) builder.AddAttribute(105, nameof(HxMultiSelectInternal.ItemsToRender), itemsToRender); builder.AddAttribute(106, nameof(HxMultiSelectInternal.TextSelector), TextSelector); builder.AddAttribute(107, nameof(HxMultiSelectInternal.ValueSelector), ValueSelector); - builder.AddAttribute(108, nameof(HxMultiSelectInternal.Value), Value); + builder.AddAttribute(108, nameof(HxMultiSelectInternal.SelectedValues), Value); builder.AddAttribute(109, nameof(HxMultiSelectInternal.NullDataText), NullDataText); - builder.AddAttribute(110, nameof(HxMultiSelectInternal.ItemSelectionChanged), EventCallback.Factory.Create.SelectionChangedArgs>(this, args => HandleItemSelectionChanged(args.Checked, args.Item))); + builder.AddAttribute(110, nameof(HxMultiSelectInternal.OnItemsSelectionChanged), EventCallback.Factory.Create.SelectionChangedArgs>(this, args => HandleItemSelectionChanged(args.ItemsSelected, args.ItemsDeselected))); builder.AddAttribute(111, nameof(HxMultiSelectInternal.InputGroupStartText), InputGroupStartText); builder.AddAttribute(112, nameof(HxMultiSelectInternal.InputGroupStartTemplate), InputGroupStartTemplate); builder.AddAttribute(113, nameof(HxMultiSelectInternal.InputGroupEndText), InputGroupEndText); builder.AddAttribute(114, nameof(HxMultiSelectInternal.InputGroupEndTemplate), InputGroupEndTemplate); + builder.AddAttribute(115, nameof(HxMultiSelectInternal.AllowFiltering), AllowFilteringEffective); + builder.AddAttribute(116, nameof(HxMultiSelectInternal.FilterPredicate), FilterPredicate); + builder.AddAttribute(117, nameof(HxMultiSelectInternal.ClearFilterOnHide), ClearFilterOnHideEffective); + builder.AddAttribute(118, nameof(HxMultiSelectInternal.FilterEmptyResultTemplate), FilterEmptyResultTemplate); + builder.AddAttribute(119, nameof(HxMultiSelectInternal.FilterEmptyResultText), FilterEmptyResultText); + builder.AddAttribute(120, nameof(HxMultiSelectInternal.AllowSelectAll), AllowSelectAllEffective); + builder.AddAttribute(121, nameof(HxMultiSelectInternal.SelectAllText), SelectAllText); + builder.AddAttribute(122, nameof(HxMultiSelectInternal.FilterSearchIcon), FilterSearchIconEffective); + builder.AddAttribute(123, nameof(HxMultiSelectInternal.FilterClearIcon), FilterClearIconEffective); + builder.AddAttribute(124, nameof(HxMultiSelectInternal.InputSizeEffective), ((IInputWithSize)this).InputSizeEffective); builder.AddMultipleAttributes(200, this.AdditionalAttributes); @@ -202,7 +265,8 @@ private string GetInputText() { return InputText; } - else if ((InputTextSelector is null) || (Data is null) || (CurrentValue is null)) + + if ((InputTextSelector is null) || (Data is null) || (CurrentValue is null)) { return CurrentValueAsString; } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.nongeneric.cs b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.nongeneric.cs index 731cf6bb2..5db935bdb 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.nongeneric.cs +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.nongeneric.cs @@ -15,6 +15,9 @@ static HxMultiSelect() Defaults = new MultiSelectSettings() { InputSize = InputSize.Regular, + AllowFiltering = false, + AllowSelectAll = false, + ClearFilterOnHide = true }; } } diff --git a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor index d34819069..4326bc3f6 100644 --- a/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor +++ b/Havit.Blazor.Components.Web.Bootstrap/Forms/Internal/HxMultiSelectInternal.razor @@ -6,24 +6,22 @@ bool enabled = EnabledEffective && (ItemsToRender != null); } -