Skip to content

Commit

Permalink
feat(MultiSelect): support Flags attribute (#5253)
Browse files Browse the repository at this point in the history
* doc: 文档格式化

* feat: 支持 Flags 参数

* doc: 增加示例

* doc: 更新示例

* feat: 枚举类型支持 Flags 标签

* test: 增加单元测试
  • Loading branch information
ArgoZhang authored Jan 31, 2025
1 parent ae7a53c commit 3a219b6
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 44 deletions.
63 changes: 39 additions & 24 deletions src/BootstrapBlazor.Server/Components/Samples/MultiSelects.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,31 @@

<h4>@Localizer["MultiSelectsDescription"]</h4>

<DemoBlock Title="@Localizer["MultiSelectColorTitle"]" Introduction="@Localizer["MultiSelectColorIntro"]" Name="Color">
@* <DemoBlock Title="@Localizer["MultiSelectColorTitle"]" Introduction="@Localizer["MultiSelectColorIntro"]" Name="Color">
<div class="row g-3">
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Items="@Items1" />
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Primary" Items="@Items2" />
<MultiSelect TValue="string" Color="Color.Primary" Items="@Items2"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Success" Items="@Items3" />
<MultiSelect TValue="string" Color="Color.Success" Items="@Items3"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Danger" Items="@Items4" />
<MultiSelect TValue="string" Color="Color.Danger" Items="@Items4"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Warning" Items="@Items5" />
<MultiSelect TValue="string" Color="Color.Warning" Items="@Items5"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Info" Items="@Items6" />
<MultiSelect TValue="string" Color="Color.Info" Items="@Items6"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Secondary" Items="@Items7" />
<MultiSelect TValue="string" Color="Color.Secondary" Items="@Items7"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Color="Color.Dark" Items="@Items8" />
<MultiSelect TValue="string" Color="Color.Dark" Items="@Items8"></MultiSelect>
</div>
</div>
</DemoBlock>
Expand All @@ -40,7 +40,7 @@
</section>
<div class="row g-3">
<div class="col-12 col-sm-4">
<MultiSelect TValue="string" Items="@Items1" IsSingleLine="true" />
<MultiSelect TValue="string" Items="@Items1" IsSingleLine="true"></MultiSelect>
</div>
</div>
</DemoBlock>
Expand All @@ -51,12 +51,12 @@
</section>
<div class="row g-3">
<div class="col-12 col-sm-6">
<MultiSelect Items="@Items1" @bind-Value="@SelectedItemsValue" />
<MultiSelect Items="@Items1" @bind-Value="@SelectedItemsValue"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddItems" class="me-1" />
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveItems" />
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearItems" />
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddItems" class="me-1"></Button>
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveItems"></Button>
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearItems"></Button>
</div>
</div>
<section ignore>@SelectedItemsValue</section>
Expand All @@ -66,12 +66,12 @@
<section ignore>@((MarkupString)Localizer["MultiSelectBindingCollectionDescription"].Value)</section>
<div class="row g-3">
<div class="col-12 col-sm-6">
<MultiSelect Items="@Items" @bind-Value="@SelectedArrayValues" Max="4" Min="2" />
<MultiSelect Items="@Items" @bind-Value="@SelectedArrayValues" Max="4" Min="2"></MultiSelect>
</div>
<div class="col-12 col-sm-6">
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddListItems" class="me-1" />
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveListItems" />
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearListItems" />
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddListItems" class="me-1"></Button>
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveListItems"></Button>
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearListItems"></Button>
</div>
</div>
<section ignore>@(string.Join(",", SelectedArrayValues))</section>
Expand All @@ -84,21 +84,36 @@
<MultiSelect Items="@LongItems" @bind-Value="@SelectedIntArrayValues" />
</div>
<div class="col-12 col-sm-6">
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddArrayItems" class="me-1" />
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveArrayItems" />
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearArrayItems" />
<Button Icon="fa-solid fa-plus" Text="@Localizer["MultiSelectAdd"]" OnClick="@AddArrayItems" class="me-1"></Button>
<Button Icon="fa-solid fa-minus" Text="@Localizer["MultiSelectDecrease"]" OnClick="@RemoveArrayItems"></Button>
<Button Icon="fa-regular fa-trash-can" Text="@Localizer["MultiSelectClean"]" OnClick="@ClearArrayItems"></Button>
</div>
</div>
<section ignore>@(string.Join(",", SelectedIntArrayValues))</section>
</DemoBlock>

<DemoBlock Title="@Localizer["MultiSelectBindingEnumCollectionTitle"]" Introduction="@Localizer["MultiSelectBindingEnumCollectionIntro"]" Name="BindingEnumCollection">
<section ignore>@((MarkupString)Localizer["MultiSelectBindingEnumCollectionDescription"].Value)</section>
<MultiSelect @bind-Value="@SelectedEnumValues" />
<MultiSelect @bind-Value="@SelectedEnumValues"></MultiSelect>
<section ignore>@(string.Join(",", SelectedEnumValues))</section>
</DemoBlock> *@

<DemoBlock Title="@Localizer["MultiSelectFlagsEnumTitle"]" Introduction="@Localizer["MultiSelectFlagsEnumIntro"]"
Name="Flags">
<section ignore>
<Pre>[Flags]
private enum MultiSelectEnumFoo
{
One = 1,
Two = 2,
Three = 4,
Four = 8
}</Pre>
</section>
<MultiSelect @bind-Value="@EnumFoo"></MultiSelect>
</DemoBlock>

<DemoBlock Title="@Localizer["MultiSelectSearchTitle"]" Introduction="@Localizer["MultiSelectSearchIntro"]" Name="Search">
@* <DemoBlock Title="@Localizer["MultiSelectSearchTitle"]" Introduction="@Localizer["MultiSelectSearchIntro"]" Name="Search">
<section ignore>@((MarkupString)Localizer["MultiSelectSearchDescription"].Value)</section>
<MultiSelect Items="@Items" @bind-Value="@SelectedSearchItemsValue" ShowSearch="true" OnSearchTextChanged="@OnSearch" />
<section ignore>@SelectedSearchItemsValue</section>
Expand Down Expand Up @@ -192,7 +207,7 @@
<section ignore>@((MarkupString)Localizer["MultiSelectCascadingDescription"].Value)</section>
<div class="row g-3">
<div class="col-12 col-sm-6">
<Select TValue="string" Items="@CascadingItems2" OnSelectedItemChanged="@OnCascadeBindSelectClick" />
<Select TValue="string" Items="@_cascadingItems2" OnSelectedItemChanged="@OnCascadeBindSelectClick"></Select>
</div>
<div class="col-12 col-sm-6">
<MultiSelect TValue="string" Items="@CascadingItems1" />
Expand Down Expand Up @@ -253,7 +268,7 @@
<Display Value="@_editString"></Display>
</div>
</div>
</DemoBlock>
</DemoBlock> *@

<AttributeTable Items="@GetAttributes()" />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,24 @@ public partial class MultiSelects

private string SelectedItemsValue { get; set; } = "Beijing";

private IEnumerable<string> SelectedArrayValues { get; set; } = Enumerable.Empty<string>();
private IEnumerable<string> SelectedArrayValues { get; set; } = [];

private IEnumerable<EnumEducation> SelectedEnumValues { get; set; } = new List<EnumEducation>
{
EnumEducation.Middle, EnumEducation.Primary
};

private MultiSelectEnumFoo EnumFoo { get; set; } = MultiSelectEnumFoo.One | MultiSelectEnumFoo.Two;

[Flags]
private enum MultiSelectEnumFoo
{
One = 1,
Two = 2,
Three = 4,
Four = 8
}

[NotNull]
private ConsoleLogger? Logger { get; set; }

Expand Down Expand Up @@ -101,7 +112,7 @@ private async Task<SelectedItem> OnEditCallback(string value)
{
await Task.Delay(100);

var item = EditableItems.Find(i => i.Text.Equals(value, System.StringComparison.OrdinalIgnoreCase));
var item = EditableItems.Find(i => i.Text.Equals(value, StringComparison.OrdinalIgnoreCase));
if (item == null)
{
item = new SelectedItem(value, value);
Expand All @@ -120,7 +131,7 @@ private async Task<SelectedItem> OnEditCallback(string value)
new("Ningbo", "宁波") {GroupName = "华东", Active = true }
];

private readonly SelectedItem[] CascadingItems2 =
private readonly SelectedItem[] _cascadingItems2 =
[
new("", "请选择 ..."),
new("Beijing", "北京") { Active = true },
Expand Down Expand Up @@ -209,12 +220,12 @@ private void AddListItems()

private void RemoveListItems()
{
SelectedArrayValues = new[] { "Beijing" };
SelectedArrayValues = ["Beijing"];
}

private void ClearListItems()
{
SelectedArrayValues = Enumerable.Empty<string>();
SelectedArrayValues = [];
}

private void AddArrayItems()
Expand All @@ -236,7 +247,7 @@ private IEnumerable<SelectedItem> OnSearch(string searchText)
{
Logger.Log($"{Localizer["MultiSelectSearchLog"]}{searchText}");
SearchItemsSource ??= GenerateItems();
return SearchItemsSource.Where(i => i.Text.Contains(searchText, System.StringComparison.OrdinalIgnoreCase));
return SearchItemsSource.Where(i => i.Text.Contains(searchText, StringComparison.OrdinalIgnoreCase));
}

private Task OnSelectedItemsChanged8(IEnumerable<SelectedItem> items)
Expand Down
2 changes: 2 additions & 0 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2941,6 +2941,8 @@
"MultiSelectSearchTitle": "Search function",
"MultiSelectSearchIntro": "Turn on search by setting the <code>ShowSearch</code> value",
"MultiSelectSearchDescription": "In this example, the search callback delegate method is set <code>onSearchTextChanged</code> to customize search results if the display text is used internally to make a fuzzy match when not set",
"MultiSelectFlagsEnumTitle": "Flags Enum",
"MultiSelectFlagsEnumIntro": "When the binding value is an <code>Enum</code> data type, if it has a <code>Flags</code> tag, multiple selection mode is automatically supported",
"MultiSelectGroupTitle": "Grouping",
"MultiSelectGroupIntro": "Alternatives are presented in groups",
"MultiSelectDisableTitle": "Disable the feature",
Expand Down
2 changes: 2 additions & 0 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2941,6 +2941,8 @@
"MultiSelectSearchTitle": "搜索功能",
"MultiSelectSearchIntro": "通过设置 <code>ShowSearch</code> 值开启搜索功能",
"MultiSelectSearchDescription": "本例中设置搜索回调委托方法 <code>OnSearchTextChanged</code> 进行自定义搜索结果,如果未设置时内部使用显示文本进行模糊匹配",
"MultiSelectFlagsEnumTitle": "Flags 枚举",
"MultiSelectFlagsEnumIntro": "绑定值为 <code>Enum</code> 数据类型时,如果枚举有 <code>Flags</code> 标签时,自动支持多选模式",
"MultiSelectGroupTitle": "分组",
"MultiSelectGroupIntro": "通过设置 <code>GroupName</code> 将下拉框中的备选项进行分组显示",
"MultiSelectDisableTitle": "禁用功能",
Expand Down
25 changes: 16 additions & 9 deletions src/BootstrapBlazor/Components/Select/MultiSelect.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

using Microsoft.Extensions.Localization;
using System.Collections;
using System.Collections.Specialized;
using System.Reflection;

namespace BootstrapBlazor.Components;

Expand Down Expand Up @@ -234,13 +236,15 @@ protected override void OnParametersSet()
ResetRules();

_itemsCache = null;

// 通过 Value 对集合进行赋值
if (PreviousValue != CurrentValueAsString)
var _currentValue = CurrentValueAsString;
if (PreviousValue != _currentValue)
{
PreviousValue = CurrentValueAsString;
var list = CurrentValueAsString.Split(',', StringSplitOptions.RemoveEmptyEntries);
PreviousValue = _currentValue;
var list = _currentValue.Split(',', StringSplitOptions.RemoveEmptyEntries);
SelectedItems.Clear();
SelectedItems.AddRange(Rows.Where(item => list.Any(i => i == item.Value)));
SelectedItems.AddRange(Rows.Where(item => list.Any(i => i.Trim() == item.Value)));
}
}

Expand Down Expand Up @@ -397,14 +401,13 @@ private void ResetRules()

private async Task SetValue()
{
var typeValue = NullableUnderlyingType ?? typeof(TValue);
if (typeValue == typeof(string))
if (ValueType == typeof(string))
{
CurrentValueAsString = string.Join(",", SelectedItems.Select(i => i.Value));
}
else if (typeValue.IsGenericType || typeValue.IsArray)
else if (ValueType.IsGenericType || ValueType.IsArray)
{
var t = typeValue.IsGenericType ? typeValue.GenericTypeArguments[0] : typeValue.GetElementType()!;
var t = ValueType.IsGenericType ? ValueType.GenericTypeArguments[0] : ValueType.GetElementType()!;
var listType = typeof(List<>).MakeGenericType(t);
var instance = (IList)Activator.CreateInstance(listType, SelectedItems.Count)!;

Expand All @@ -415,7 +418,11 @@ private async Task SetValue()
instance.Add(val);
}
}
CurrentValue = (TValue)(typeValue.IsGenericType ? instance : listType.GetMethod("ToArray")!.Invoke(instance, null)!);
CurrentValue = (TValue)(ValueType.IsGenericType ? instance : listType.GetMethod("ToArray")!.Invoke(instance, null)!);
}
else if (ValueType.IsFlagEnum())
{
CurrentValue = (TValue?)SelectedItems.ParseFlagEnum<TValue>(ValueType);
}

if (ValidateForm == null && (Min > 0 || Max > 0))
Expand Down
35 changes: 30 additions & 5 deletions src/BootstrapBlazor/Extensions/EnumExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,7 @@ public static List<SelectedItem> ToSelectList(this Type type, SelectedItem? addi
if (type.IsEnum())
{
var t = Nullable.GetUnderlyingType(type) ?? type;
foreach (var field in Enum.GetNames(t))
{
var desc = Utility.GetDisplayName(t, field);
ret.Add(new SelectedItem(field, desc));
}
ret.AddRange(from field in Enum.GetNames(t) let desc = Utility.GetDisplayName(t, field) select new SelectedItem(field, desc));
}
return ret;
}
Expand Down Expand Up @@ -114,4 +110,33 @@ public static bool IsEnum(this Type? type)
}
return ret;
}

/// <summary>
/// 判断类型是否为 Flag 枚举类型
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static bool IsFlagEnum(this Type? type) => type != null && IsEnum(type) && type.GetCustomAttribute<FlagsAttribute>() != null;

/// <summary>
/// 将 <see cref="IEnumerable{T}"/> 集合转换为 Flag 枚举值
/// </summary>
/// <param name="items"></param>
/// <param name="type"></param>
/// <returns></returns>
internal static object? ParseFlagEnum<TValue>(this IEnumerable<SelectedItem> items, Type type)
{
TValue? v = default;
if (type.IsFlagEnum())
{
foreach (var item in items)
{
if (Enum.TryParse(type, item.Value, true, out var val))
{
v = (TValue)Enum.ToObject(type, Convert.ToInt32(v) | Convert.ToInt32(val));
}
}
}
return v;
}
}
4 changes: 4 additions & 0 deletions src/BootstrapBlazor/Utils/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -830,6 +830,10 @@ public static string Format(object? source, IFormatProvider provider)
}
}
}
else if (typeValue.IsFlagEnum())
{
ret = value!.ToString();
}
return ret;
}

Expand Down
27 changes: 27 additions & 0 deletions test/UnitTest/Components/MultiSelectTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,33 @@ public void EnumValue_Ok()
Assert.Contains("multi-select", cut.Markup);
}

[Fact]
public async Task FlagEnum_Ok()
{
var value = MockFlagEnum.One | MockFlagEnum.Two;
var cut = Context.RenderComponent<MultiSelect<MockFlagEnum>>(pb =>
{
pb.Add(a => a.Value, value);
});
var values = cut.FindAll(".multi-select-items .multi-select-item");
Assert.Equal(2, values.Count);

// 选中第四个
var item = cut.FindAll(".dropdown-menu .dropdown-item").Last();
await cut.InvokeAsync(() => item.Click());
values = cut.FindAll(".multi-select-items .multi-select-item");
Assert.Equal(3, values.Count);
}

[Flags]
private enum MockFlagEnum
{
One = 1,
Two = 2,
Three = 4,
Four = 8
}

[Fact]
public void Group_Ok()
{
Expand Down

0 comments on commit 3a219b6

Please sign in to comment.