Skip to content

Commit 3cf0a8f

Browse files
authored
feat(CheckboxListGeneric): add CheckboxListGeneric component (#4905)
* refactor: 增加 Key 关键字 * feat: 增加 CheckboxGeneric 组件 * refactor: 更新泛型 CheckboxList 组件 * doc: 更新示例文档 * test: 增加单元测试 * feat: 增加 CheckboxListGeneric 组件 * test: 增加单元测试
1 parent abb4b5a commit 3cf0a8f

File tree

6 files changed

+575
-1
lines changed

6 files changed

+575
-1
lines changed

src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@
120120
</ValidateForm>
121121
</DemoBlock>
122122

123+
<DemoBlock Title="@Localizer["GenericTitle"]" Introduction="@Localizer["GenericIntro"]" Name="Generic">
124+
<CheckboxListGeneric @bind-Value="@_selectedFoos" Items="@GenericItems" />
125+
<section ignore>
126+
@if (_selectedFoos != null)
127+
{
128+
foreach (var item in _selectedFoos)
129+
{
130+
<div>@item.Name</div>
131+
}
132+
}
133+
</section>
134+
</DemoBlock>
135+
123136
<AttributeTable Items="@GetAttributes()" />
124137

125138
<EventTable Items="@GetEvents()" />

src/BootstrapBlazor.Server/Components/Samples/CheckboxLists.razor.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public partial class CheckboxLists
3737
[NotNull]
3838
private IEnumerable<SelectedItem>? Items5 { get; set; }
3939

40+
[NotNull]
41+
private IEnumerable<SelectedItem<Foo>>? GenericItems { get; set; }
42+
43+
private List<Foo>? _selectedFoos;
44+
4045
/// <summary>
4146
/// OnInitialized method
4247
/// </summary>
@@ -89,6 +94,22 @@ protected override void OnInitialized()
8994
FooItems = Foo.GenerateHobbies(LocalizerFoo);
9095
}
9196

97+
/// <summary>
98+
/// <inheritdoc/>
99+
/// </summary>
100+
/// <returns></returns>
101+
protected override async Task OnInitializedAsync()
102+
{
103+
await base.OnInitializedAsync();
104+
105+
GenericItems = new List<SelectedItem<Foo>>()
106+
{
107+
new() { Text = Localizer["item1"], Value = new Foo() { Name = LocalizerFoo["Foo.Name", "001"] } },
108+
new() { Text = Localizer["item2"], Value = new Foo() { Name = LocalizerFoo["Foo.Name", "002"] } },
109+
new() { Text = Localizer["item3"], Value = new Foo() { Name = LocalizerFoo["Foo.Name", "003"] } },
110+
};
111+
}
112+
92113
[NotNull]
93114
private Foo? Model { get; set; }
94115

src/BootstrapBlazor/Components/Checkbox/CheckboxList.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ else
2525
<div @attributes="@AdditionalAttributes" id="@Id" class="@ClassString" tabindex="0" hidefocus="true">
2626
@foreach (var item in Items)
2727
{
28-
<div class="@CheckboxItemClassString">
28+
<div @key="item" class="@CheckboxItemClassString">
2929
<Checkbox TValue="bool" IsDisabled="GetDisabledState(item)"
3030
ShowAfterLabel="true" ShowLabel="false" ShowLabelTooltip="ShowLabelTooltip"
3131
DisplayText="@item.Text" OnBeforeStateChanged="_onBeforeStateChangedCallback!"
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@namespace BootstrapBlazor.Components
2+
@typeparam TValue
3+
@inherits ValidateBase<List<TValue>>
4+
5+
@if (IsShowLabel)
6+
{
7+
<BootstrapLabel required="@Required" for="@Id" ShowLabelTooltip="ShowLabelTooltip" Value="@DisplayText" />
8+
}
9+
10+
@if (IsButton)
11+
{
12+
<div @attributes="@AdditionalAttributes" class="@ButtonClassString">
13+
<div class="@ButtonGroupClassString" role="group">
14+
@foreach (var item in Items)
15+
{
16+
<DynamicElement TagName="span" TriggerClick="!IsDisabled" OnClick="() => OnClick(item)" class="@GetButtonItemClassString(item)">
17+
@item.Text
18+
</DynamicElement>
19+
}
20+
</div>
21+
</div>
22+
}
23+
else
24+
{
25+
<div @attributes="@AdditionalAttributes" id="@Id" class="@ClassString" tabindex="0" hidefocus="true">
26+
@foreach (var item in Items)
27+
{
28+
<div @key="item" class="@CheckboxItemClassString">
29+
<Checkbox TValue="bool" IsDisabled="GetDisabledState(item)"
30+
ShowAfterLabel="true" ShowLabel="false" ShowLabelTooltip="ShowLabelTooltip"
31+
DisplayText="@item.Text" OnBeforeStateChanged="_onBeforeStateChangedCallback!"
32+
Value="@item.Active" OnStateChanged="@((state, v) => OnStateChanged(item, v))"></Checkbox>
33+
</div>
34+
}
35+
</div>
36+
}
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
using Microsoft.Extensions.Localization;
7+
using System.Reflection;
8+
9+
namespace BootstrapBlazor.Components;
10+
11+
/// <summary>
12+
/// CheckboxList 组件基类
13+
/// </summary>
14+
public partial class CheckboxListGeneric<TValue> : IModelEqualityComparer<TValue>
15+
{
16+
/// <summary>
17+
/// 获得 组件样式
18+
/// </summary>
19+
private string? ClassString => CssBuilder.Default("checkbox-list form-control")
20+
.AddClass("no-border", !ShowBorder && ValidCss != "is-invalid")
21+
.AddClass("is-vertical", IsVertical)
22+
.AddClass(CssClass).AddClass(ValidCss)
23+
.Build();
24+
25+
/// <summary>
26+
/// 获得 组件内部 Checkbox 项目样式
27+
/// </summary>
28+
protected string? CheckboxItemClassString => CssBuilder.Default("checkbox-item")
29+
.AddClass(CheckboxItemClass)
30+
.Build();
31+
32+
private string? ButtonClassString => CssBuilder.Default("checkbox-list is-button")
33+
.AddClassFromAttributes(AdditionalAttributes)
34+
.Build();
35+
36+
private string? ButtonGroupClassString => CssBuilder.Default("btn-group")
37+
.AddClass("disabled", IsDisabled)
38+
.AddClass("btn-group-vertical", IsVertical)
39+
.Build();
40+
41+
private string? GetButtonItemClassString(SelectedItem<TValue> item) => CssBuilder.Default("btn")
42+
.AddClass($"active bg-{Color.ToDescriptionString()}", IsEquals(item.Value))
43+
.Build();
44+
45+
/// <summary>
46+
/// 获得/设置 数据主键标识标签 默认为 <see cref="KeyAttribute"/><code><br /></code>用于判断数据主键标签,如果模型未设置主键时可使用 <see cref="ModelEqualityComparer"/> 参数自定义判断 <code><br /></code>数据模型支持联合主键
47+
/// </summary>
48+
[Parameter]
49+
[NotNull]
50+
public Type? CustomKeyAttribute { get; set; } = typeof(KeyAttribute);
51+
52+
/// <summary>
53+
/// 获得/设置 比较数据是否相同回调方法 默认为 null
54+
/// <para>提供此回调方法时忽略 <see cref="CustomKeyAttribute"/> 属性</para>
55+
/// </summary>
56+
[Parameter]
57+
public Func<TValue, TValue, bool>? ModelEqualityComparer { get; set; }
58+
59+
/// <summary>
60+
/// 获得/设置 数据源
61+
/// </summary>
62+
[Parameter]
63+
[NotNull]
64+
public IEnumerable<SelectedItem<TValue>>? Items { get; set; }
65+
66+
/// <summary>
67+
/// 获得/设置 是否为按钮样式 默认 false
68+
/// </summary>
69+
[Parameter]
70+
public bool IsButton { get; set; }
71+
72+
/// <summary>
73+
/// 获得/设置 Checkbox 组件布局样式
74+
/// </summary>
75+
[Parameter]
76+
public string? CheckboxItemClass { get; set; }
77+
78+
/// <summary>
79+
/// 获得/设置 是否显示边框 默认为 true
80+
/// </summary>
81+
[Parameter]
82+
public bool ShowBorder { get; set; } = true;
83+
84+
/// <summary>
85+
/// 获得/设置 是否为竖向排列 默认为 false
86+
/// </summary>
87+
[Parameter]
88+
public bool IsVertical { get; set; }
89+
90+
/// <summary>
91+
/// 获得/设置 按钮颜色 默认为 None 未设置
92+
/// </summary>
93+
[Parameter]
94+
public Color Color { get; set; }
95+
96+
/// <summary>
97+
/// 获得/设置 SelectedItemChanged 方法
98+
/// </summary>
99+
[Parameter]
100+
public Func<IEnumerable<SelectedItem<TValue>>, List<TValue>, Task>? OnSelectedChanged { get; set; }
101+
102+
/// <summary>
103+
/// 获得/设置 最多选中数量
104+
/// </summary>
105+
[Parameter]
106+
public int MaxSelectedCount { get; set; }
107+
108+
/// <summary>
109+
/// 获得/设置 超过最大选中数量时回调委托
110+
/// </summary>
111+
[Parameter]
112+
public Func<Task>? OnMaxSelectedCountExceed { get; set; }
113+
114+
[Inject]
115+
[NotNull]
116+
private IStringLocalizerFactory? LocalizerFactory { get; set; }
117+
118+
/// <summary>
119+
/// 获得 当前选项是否被禁用
120+
/// </summary>
121+
/// <param name="item"></param>
122+
/// <returns></returns>
123+
protected bool GetDisabledState(SelectedItem<TValue> item) => IsDisabled || item.IsDisabled;
124+
125+
private Func<CheckboxState, Task<bool>>? _onBeforeStateChangedCallback;
126+
127+
/// <summary>
128+
/// OnInitialized 方法
129+
/// </summary>
130+
protected override void OnInitialized()
131+
{
132+
base.OnInitialized();
133+
134+
// 处理 Required 标签
135+
if (EditContext != null && FieldIdentifier != null)
136+
{
137+
var pi = FieldIdentifier.Value.Model.GetType().GetPropertyByName(FieldIdentifier.Value.FieldName);
138+
if (pi != null)
139+
{
140+
var required = pi.GetCustomAttribute<RequiredAttribute>(true);
141+
if (required != null)
142+
{
143+
Rules.Add(new RequiredValidator()
144+
{
145+
LocalizerFactory = LocalizerFactory,
146+
ErrorMessage = required.ErrorMessage,
147+
AllowEmptyString = required.AllowEmptyStrings
148+
});
149+
}
150+
}
151+
}
152+
}
153+
154+
/// <summary>
155+
/// OnParametersSet 方法
156+
/// </summary>
157+
protected override void OnParametersSet()
158+
{
159+
base.OnParametersSet();
160+
161+
if (IsButton && Color == Color.None)
162+
{
163+
Color = Color.Primary;
164+
}
165+
166+
Items ??= [];
167+
168+
_onBeforeStateChangedCallback = MaxSelectedCount > 0 ? new Func<CheckboxState, Task<bool>>(OnBeforeStateChanged) : null;
169+
170+
// set item active
171+
if (Value != null)
172+
{
173+
var item = Items.FirstOrDefault(i => IsEquals(i.Value));
174+
if (item != null)
175+
{
176+
item.Active = true;
177+
}
178+
}
179+
}
180+
181+
private bool IsEquals(TValue? val) => Value != null && Value.Find(v => Equals(v, val)) != null;
182+
183+
private async Task<bool> OnBeforeStateChanged(CheckboxState state)
184+
{
185+
var ret = true;
186+
if (state == CheckboxState.Checked)
187+
{
188+
var items = Items.Where(i => i.Active).ToList();
189+
ret = items.Count < MaxSelectedCount;
190+
}
191+
192+
if (!ret && OnMaxSelectedCountExceed != null)
193+
{
194+
await OnMaxSelectedCountExceed();
195+
}
196+
return ret;
197+
}
198+
199+
/// <summary>
200+
/// Checkbox 组件选项状态改变时触发此方法
201+
/// </summary>
202+
/// <param name="item"></param>
203+
/// <param name="v"></param>
204+
private async Task OnStateChanged(SelectedItem<TValue> item, bool v)
205+
{
206+
item.Active = v;
207+
var vals = new List<TValue?>();
208+
if (Value != null)
209+
{
210+
vals.AddRange(Value);
211+
}
212+
213+
var val = vals.Find(i => IsEquals(item.Value));
214+
if (v && val == null)
215+
{
216+
vals.Add(item.Value);
217+
}
218+
else
219+
{
220+
vals.Remove(val);
221+
}
222+
223+
CurrentValue = vals;
224+
225+
if (OnSelectedChanged != null)
226+
{
227+
await OnSelectedChanged(Items, CurrentValue);
228+
}
229+
else
230+
{
231+
StateHasChanged();
232+
}
233+
}
234+
235+
/// <summary>
236+
/// 点击选择框方法
237+
/// </summary>
238+
private Task OnClick(SelectedItem<TValue> item) => OnStateChanged(item, !item.Active);
239+
240+
/// <summary>
241+
/// <inheritdoc/>
242+
/// </summary>
243+
public bool Equals(TValue? x, TValue? y) => this.Equals<TValue>(x, y);
244+
}

0 commit comments

Comments
 (0)