Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HxMultiSelect] filtering and select all #617

Merged
merged 37 commits into from
Oct 22, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
fcce455
Added HxMultiSelect filtering
Oct 8, 2023
64ff133
Moved filtering box into dropdown
Oct 10, 2023
ad7f2ef
Reverted styling changes
Oct 10, 2023
e725c0c
Changed filter text and auto-focusing
Oct 10, 2023
74e58a5
Added empty template and changed hide/show handlers
Oct 10, 2023
a659d9f
Added default filter predicate
Oct 10, 2023
8632672
Changed filtering enablement property
Oct 10, 2023
3d48a11
Added empty template
Oct 10, 2023
5612d24
Changed default filter to use IsNullOrEmpty
Oct 10, 2023
6769c73
Added select all
Oct 10, 2023
dfb9f91
Fixed default select all text value
Oct 10, 2023
c99868d
Removed unused code
Oct 18, 2023
ec53d47
Renamed function
Oct 18, 2023
3e04982
Added string localisation to select all default text
Oct 18, 2023
1466054
Added select all default text to resources
Oct 18, 2023
5e92a17
Removed the unused shouldToggle option
Oct 18, 2023
1093cc6
Changed default select all text to '-select all-'
Oct 18, 2023
6553958
Empty template now has a default template and text parameter. Fixed l…
Oct 18, 2023
55a4cec
Added AllowFiltering and AllowSelectAll as settings
Oct 18, 2023
1ce4766
Removed OnShown and OnHidden event callbacks
Oct 18, 2023
4b3d6a4
Changed default ClearFilterOnHide to true
Oct 18, 2023
e3fe12e
Added ClearFilterOnHide=false to test example
Oct 18, 2023
c0bdb48
Added documentation
Oct 18, 2023
1b8cf70
HxMultiSelect_Demo_TemplatedFiltering - project coding standards - op…
hakenr Oct 19, 2023
3794959
HxMultiSelect - FilterPredicate doc adjustment
hakenr Oct 19, 2023
48c6b59
Removed all SelectAllChanged event callbacks
Oct 19, 2023
c800ecb
Merge branch 'feature/HxMultiSelect-filtering' of https://github.com/…
Oct 19, 2023
a57f860
Added filter search and clear icons
Oct 19, 2023
159c1d2
Reworked select all item selection callbacks
Oct 20, 2023
b2de43b
Wrapped select all button in a li
Oct 20, 2023
e54ae00
Added li around empty filter result
Oct 20, 2023
bef368a
Added padding to filter input.
Oct 20, 2023
34e7033
Filter input now respects parent input size
Oct 20, 2023
5696023
HxMultiSelect - dispose fix
hakenr Oct 22, 2023
ab61f92
HxMultiSelect - doc & demos tuning
hakenr Oct 22, 2023
1144ed2
HxMultiSelect - code-cleanup
hakenr Oct 22, 2023
3ab65e2
HxMultiSelect - localizations
hakenr Oct 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 47 additions & 24 deletions BlazorAppTest/Pages/HxMultiSelectTest.razor
Original file line number Diff line number Diff line change
@@ -1,38 +1,61 @@
@page "/HxMultiSelectTest"
@using System.Globalization

<h1>HxCheckboxList</h1>
<h1>HxMultiSelect</h1>

<p>Selected values: @String.Join(", ", model.CultureInfos ?? Enumerable.Empty<string>())</p>

<HxSwitch Text="Enabled" @bind-Value="@enabled" />

<EditForm Model="@model">
<HxMultiSelect TItem="CultureInfo" TValue="string" Label="Cultures" EmptyText="-- choose here --" TextSelector="@(item => item.EnglishName)" ValueSelector="@(item => item.EnglishName)" Data="@data" @bind-Value="@model.CultureInfos" NullDataText="Loading languages..." Enabled="@enabled" InputSize="InputSize.Small" />

<HxMultiSelect TItem="CultureInfo" TValue="string" Label="Cultures" EmptyText="-- choose here --" TextSelector="@(item => item.EnglishName)" ValueSelector="@(item => item.EnglishName)" Data="@data" @bind-Value="@model.CultureInfos" NullDataText="Loading languages..." Enabled="@enabled" />
<HxMultiSelect TItem="CultureInfo" TValue="string" Label="Cultures" EmptyText="-- choose here --" TextSelector="@(item => item.EnglishName)" ValueSelector="@(item => item.EnglishName)" Data="@data" @bind-Value="@model.CultureInfos" NullDataText="Loading languages..." Enabled="@enabled" InputSize="InputSize.Large" />
</EditForm>

<p>Selected values: @String.Join(", ", model.CultureInfos ?? Enumerable.Empty<string>())</p>
<HxMultiSelect TItem="CultureInfo" TValue="string" Label="Cultures" EmptyText="-- choose here --" TextSelector="@(item => item.EnglishName)" ValueSelector="@(item => item.EnglishName)" Data="@data" @bind-Value="@model.CultureInfos" NullDataText="Loading languages..." Enabled="@enabled" InputSize="InputSize.Large" />

<!-- Multi-select with filtering -->
<HxMultiSelect TItem="CultureInfo"
TValue="string"
Label="Cultures with filtering enabled"
EmptyText="-- choose here --"
TextSelector="@(item => item.EnglishName)"
ValueSelector="@(item => item.EnglishName)"
Data="@data"
@bind-Value="@model.CultureInfos"
NullDataText="Loading languages..."
EnableFiltering="true"
FilterSelector="((item, filterText) => FilterSelector(item, filterText))"
Enabled="@enabled" />
</EditForm>

@code
{
private bool enabled = true;
private Model model = new Model();
private List<CultureInfo> 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<string> CultureInfos { get; set; }
}
private bool enabled = true;
private Model model = new Model();
private List<CultureInfo> data;

private HxMultiSelect<string, CultureInfo> hxMultiSelectFilter;

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<string> CultureInfos { get; set; }
}

private bool FilterSelector(CultureInfo item, string filter)
{
return string.IsNullOrWhiteSpace(filter) || item.EnglishName.Contains(filter, StringComparison.OrdinalIgnoreCase);
hakenr marked this conversation as resolved.
Show resolved Hide resolved
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2701,36 +2701,74 @@
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxInputDateRangeInternal.DisposeAsync">
<inheritdoc />
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.AdditionalAttributes">
<summary>
Additional attributes to be splatted onto an underlying HTML element.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupCssClass">
<summary>
Custom CSS class to render with input-group span.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupStartText">
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupEndTemplate">
<summary>
Input-group at the beginning of the input.
Input-group at the end of the input.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupEndText">
<summary>
Input-group at the end of the input.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupStartTemplate">
<summary>
Input-group at the beginning of the input.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupEndText">
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupStartText">
<summary>
Input-group at the end of the input.
Input-group at the beginning of the input.
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InputGroupEndTemplate">
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.OnHidden">
<summary>
Input-group at the end of the input.
This event is fired when a dropdown element has been hidden from the user (will wait for CSS transitions to complete).
</summary>
</member>
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.AdditionalAttributes">
<member name="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.OnShown">
<summary>
Additional attributes to be splatted onto an underlying HTML element.
This event is fired when a dropdown element has been made visible to the user (will wait for CSS transitions to complete).
</summary>
</member>
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.HandleJsHidden">
<summary>
Receives notification from JavaScript when item is hidden.
</summary>
</member>
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.HideAsync">
<summary>
Collapses the item.
</summary>
</member>
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.ShowAsync">
<summary>
Expands the item.
</summary>
</member>
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InvokeOnHiddenAsync(System.String)">
<summary>
Triggers the <see cref="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.OnHidden"/> event. Allows interception of the event in derived components.
</summary>
</member>
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.InvokeOnShownAsync(System.String)">
<summary>
Triggers the <see cref="P:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.OnShown"/> event. Allows interception of the event in derived components.
</summary>
</member>
<member name="M:Havit.Blazor.Components.Web.Bootstrap.Internal.HxMultiSelectInternal`2.OnAfterRenderAsync(System.Boolean)">
<inheritdoc cref="M:Microsoft.AspNetCore.Components.ComponentBase.OnAfterRenderAsync(System.Boolean)" />
</member>
<member name="T:Havit.Blazor.Components.Web.Bootstrap.Internal.IFormValueComponent">
<summary>
Represents properties (and methods) of a component rendering a form value (ie. form inputs).
Expand Down
30 changes: 28 additions & 2 deletions Havit.Blazor.Components.Web.Bootstrap/Forms/HxMultiSelect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ public class HxMultiSelect<TValue, TItem> : HxInputBase<List<TValue>>, IInputWit
/// </summary>
[Parameter] public RenderFragment InputGroupEndTemplate { get; set; }

[Parameter] public bool EnableFiltering { get; set; }
hakenr marked this conversation as resolved.
Show resolved Hide resolved

[Parameter] public Func<TItem, string, bool> FilterSelector { get; set; } = (_, _) => true;
hakenr marked this conversation as resolved.
Show resolved Hide resolved

private bool IsShown { get; set; }
hakenr marked this conversation as resolved.
Show resolved Hide resolved

private List<TItem> itemsToRender;
private HxMultiSelectInternal<TValue, TItem> hxMultiSelectInternalComponent;
Expand Down Expand Up @@ -152,6 +157,16 @@ private void HandleItemSelectionChanged(bool @checked, TItem item)
CurrentValue = newValue; // setter includes ValueChanged + NotifyFieldChanged
}

private void HandleOnHiddenChanged()
{
IsShown = false;
}

private void HandleOnShownChanged()
{
IsShown = true;
}

protected override bool TryParseValueFromString(string value, out List<TValue> result, out string validationErrorMessage)
{
throw new NotSupportedException();
Expand Down Expand Up @@ -181,13 +196,17 @@ protected override void BuildRenderInput(RenderTreeBuilder builder)
builder.AddAttribute(105, nameof(HxMultiSelectInternal<TValue, TItem>.ItemsToRender), itemsToRender);
builder.AddAttribute(106, nameof(HxMultiSelectInternal<TValue, TItem>.TextSelector), TextSelector);
builder.AddAttribute(107, nameof(HxMultiSelectInternal<TValue, TItem>.ValueSelector), ValueSelector);
builder.AddAttribute(108, nameof(HxMultiSelectInternal<TValue, TItem>.Value), Value);
builder.AddAttribute(108, nameof(HxMultiSelectInternal<TValue, TItem>.SelectedValues), Value);
builder.AddAttribute(109, nameof(HxMultiSelectInternal<TValue, TItem>.NullDataText), NullDataText);
builder.AddAttribute(110, nameof(HxMultiSelectInternal<TValue, TItem>.ItemSelectionChanged), EventCallback.Factory.Create<HxMultiSelectInternal<TValue, TItem>.SelectionChangedArgs>(this, args => HandleItemSelectionChanged(args.Checked, args.Item)));
builder.AddAttribute(111, nameof(HxMultiSelectInternal<TValue, TItem>.InputGroupStartText), InputGroupStartText);
builder.AddAttribute(112, nameof(HxMultiSelectInternal<TValue, TItem>.InputGroupStartTemplate), InputGroupStartTemplate);
builder.AddAttribute(113, nameof(HxMultiSelectInternal<TValue, TItem>.InputGroupEndText), InputGroupEndText);
builder.AddAttribute(114, nameof(HxMultiSelectInternal<TValue, TItem>.InputGroupEndTemplate), InputGroupEndTemplate);
builder.AddAttribute(115, nameof(HxMultiSelectInternal<TValue, TItem>.EnableFiltering), EnableFiltering);
builder.AddAttribute(116, nameof(HxMultiSelectInternal<TValue, TItem>.FilterSelector), FilterSelector);
builder.AddAttribute(117, nameof(HxMultiSelectInternal<TValue, TItem>.OnHidden), EventCallback.Factory.Create<string>(this, HandleOnHiddenChanged));
builder.AddAttribute(118, nameof(HxMultiSelectInternal<TValue, TItem>.OnShown), EventCallback.Factory.Create<string>(this, HandleOnShownChanged));

builder.AddMultipleAttributes(200, this.AdditionalAttributes);

Expand All @@ -202,7 +221,14 @@ private string GetInputText()
{
return InputText;
}
else if ((InputTextSelector is null) || (Data is null) || (CurrentValue is null))

// If filtering is enabled and the dropdown is visible then we want to display the user-entered filter text instead of the selected values summary
if (EnableFiltering && IsShown)
{
return InputText;
}
hakenr marked this conversation as resolved.
Show resolved Hide resolved

if ((InputTextSelector is null) || (Data is null) || (CurrentValue is null))
{
return CurrentValueAsString;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,23 @@
bool enabled = EnabledEffective && (ItemsToRender != null);
}

<div class="hx-multi-select dropdown" role="listbox">
<div class="@CssClassHelper.Combine((HasInputGroupsEffective ? "input-group" : null), InputGroupCssClass)" data-bs-toggle="@(enabled ? "dropdown" : null)" data-bs-auto-close="outside">
<div @ref="elementReference" class="hx-multi-select dropdown" role="listbox">
<div class="@CssClassHelper.Combine((HasInputGroupsEffective ? "input-group" : null), InputGroupCssClass)" aria-expanded="@IsShown" data-bs-toggle="@(enabled ? "dropdown" : null)" data-bs-auto-close="outside">
@if (InputGroupStartText is not null)
{
<span class="input-group-text">@InputGroupStartText</span>
}

@InputGroupStartTemplate

<input
@ref="inputElement"
type="text"
id="@InputId"
class="@InputCssClass"
value="@(((ItemsToRender == null) && !String.IsNullOrEmpty(NullDataText)) ? NullDataText : InputText)"
disabled="@(!enabled)"
readonly="true"
@attributes="this.AdditionalAttributes" />

<input type="text"
id="@InputId"
class="@InputCssClass"
value="@(((ItemsToRender == null) && !String.IsNullOrEmpty(NullDataText)) ? NullDataText : InputText)"
@oninput="HandleInputChanged"
disabled="@(!enabled)"
readonly="@(!EnableFiltering)"
@attributes="this.AdditionalAttributes" />

@InputGroupEndTemplate

Expand All @@ -36,14 +35,15 @@
<ul class="dropdown-menu"> @* Must be always rendered otherwise does not work after disable->enabled scenario *@
@if (enabled)
{
for (int i = 0; i < ItemsToRender.Count; i++)
var filteredItems = EnableFiltering ? ItemsToRender.Where(x => FilterSelector(x, InputText)).ToList() : ItemsToRender;
for (var i = 0; i < filteredItems.Count; i++)
{
string checkboxElementId = InputId + "_" + i.ToString();

var item = ItemsToRender[i];
var item = filteredItems[i];
TValue value = SelectorHelpers.GetValue<TItem, TValue>(ValueSelector, item);

bool itemSelected = Value?.Contains(value) ?? false;
bool itemSelected = SelectedValues?.Contains(value) ?? false;

<li>
<button type="button" class="dropdown-item" role="option" @onclick="async () => await HandleItemSelectionChangedAsync(!itemSelected, item)">
Expand Down
Loading