Skip to content

Commit 434f507

Browse files
trycatchfinnallyT1246ArgoZhang
authored
feat(Select): redesign IsClearable function (#5626)
* 对Select组件进行修改,设置IsClearable后,如果绑定类型可以为空,清除后清楚所有选中项而不是第一项 * 修复yige bug * refactor: 更新可为空逻辑 * test: 更新单元测试 * doc: 更新示例 * refactor: 增加 readonly 关键字 * test: 更新单元测试 * test: 更新单元测试 * refactor: 重置 SelectedItem * doc: 更新示例 * doc: 更新示例 * test: 增加不可为空整形设置 IsClearable 单元测试 --------- Co-authored-by: T1246 <T1246@XL> Co-authored-by: Argo Zhang <[email protected]>
1 parent d32b604 commit 434f507

File tree

6 files changed

+175
-53
lines changed

6 files changed

+175
-53
lines changed

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

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -87,19 +87,6 @@
8787
</div>
8888
</DemoBlock>
8989

90-
<DemoBlock Title="@Localizer["SelectsClearableTitle"]"
91-
Introduction="@Localizer["SelectsClearableIntro"]"
92-
Name="IsClearable">
93-
<div class="row g-3">
94-
<div class="col-12 col-sm-6">
95-
<Select Color="Color.Primary" IsClearable="true" Items="ClearableItems" Value="ClearableModel.Name"></Select>
96-
</div>
97-
<div class="col-12 col-sm-6">
98-
<Select Color="Color.Primary" IsClearable="true" Items="Items" Value="ClearableModel.Name"></Select>
99-
</div>
100-
</div>
101-
</DemoBlock>
102-
10390
<DemoBlock Title="@Localizer["SelectsBindingSelectedItemTitle"]"
10491
Introduction="@Localizer["SelectsBindingSelectedItemIntro"]"
10592
Name="BindingSelectedItem">
@@ -348,6 +335,28 @@
348335
</div>
349336
</DemoBlock>
350337

338+
<DemoBlock Title="@Localizer["SelectsClearableTitle"]"
339+
Introduction="@Localizer["SelectsClearableIntro"]"
340+
Name="IsClearable">
341+
<section ignore>
342+
<p>@((MarkupString)Localizer["SelectsClearableDesc"].Value)</p>
343+
</section>
344+
<div class="row g-3">
345+
<div class="col-12 col-sm-6">
346+
<Select IsClearable="true" Items="ClearableItems" Value="ClearableModel.NullableName" ShowLabel="true" DisplayText="string?"></Select>
347+
</div>
348+
<div class="col-12 col-sm-6">
349+
<Select IsClearable="true" Items="Items" Value="ClearableModel.Name" ShowLabel="true" DisplayText="string"></Select>
350+
</div>
351+
<div class="col-12 col-sm-6">
352+
<Select IsClearable="true" Items="IntItems" Value="ClearableModel.NullableCount" ShowLabel="true" DisplayText="int?"></Select>
353+
</div>
354+
<div class="col-12 col-sm-6">
355+
<Select IsClearable="true" Items="IntItems" Value="ClearableModel.Count" ShowLabel="true" DisplayText="int"></Select>
356+
</div>
357+
</div>
358+
</DemoBlock>
359+
351360
<DemoBlock Title="@Localizer["SelectsConfirmSelectTitle"]"
352361
Introduction="@Localizer["SelectsConfirmSelectIntro"]"
353362
Name="ConfirmSelect">
@@ -412,22 +421,23 @@
412421
<DemoBlock Title="@Localizer["SelectsVirtualizeTitle"]"
413422
Introduction="@Localizer["SelectsVirtualizeIntro"]"
414423
Name="IsVirtualize">
415-
<section ignore>@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)</section>
416-
417-
<div class="row mb-3">
418-
<div class="col-12 col-sm-6">
419-
<BootstrapInputGroup>
420-
<BootstrapInputGroupLabel DisplayText="ShowSearch" />
421-
<Checkbox @bind-Value="@_showSearch" />
422-
</BootstrapInputGroup>
423-
</div>
424-
<div class="col-12 col-sm-6">
425-
<BootstrapInputGroup>
426-
<BootstrapInputGroupLabel DisplayText="IsClearable" />
427-
<Checkbox @bind-Value="@_isClearable" />
428-
</BootstrapInputGroup>
424+
<section ignore>
425+
<p>@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)</p>
426+
<div class="row mb-3">
427+
<div class="col-12 col-sm-6">
428+
<BootstrapInputGroup>
429+
<BootstrapInputGroupLabel DisplayText="ShowSearch" />
430+
<Checkbox @bind-Value="@_showSearch" />
431+
</BootstrapInputGroup>
432+
</div>
433+
<div class="col-12 col-sm-6">
434+
<BootstrapInputGroup>
435+
<BootstrapInputGroupLabel DisplayText="IsClearable" />
436+
<Checkbox @bind-Value="@_isClearable" />
437+
</BootstrapInputGroup>
438+
</div>
429439
</div>
430-
</div>
440+
</section>
431441

432442
<p class="code-label">1. 使用 OnQueryAsync 作为数据源</p>
433443
<div class="row mb-3">

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public sealed partial class Selects
5151

5252
private string? _fooName;
5353

54-
private List<SelectedItem> _enumValueDemoItems = [
54+
private readonly List<SelectedItem> _enumValueDemoItems = [
5555
new("0", "Primary"),
5656
new("1", "Middle")
5757
];
@@ -101,7 +101,18 @@ private Task OnItemChanged(SelectedItem item)
101101

102102
private Foo BindingModel { get; set; } = new Foo();
103103

104-
private Foo ClearableModel { get; set; } = new Foo();
104+
private MockModel ClearableModel { get; set; } = new();
105+
106+
class MockModel
107+
{
108+
public string? NullableName { get; set; }
109+
110+
public string Name { get; set; } = "";
111+
112+
public int Count { get; set; } = 1;
113+
114+
public int? NullableCount { get; set; }
115+
}
105116

106117
private SelectedItem? Item { get; set; }
107118

@@ -230,6 +241,14 @@ private string GetSelectedBoolItemString()
230241
new("abcde", "abcde")
231242
];
232243

244+
private readonly SelectedItem[] IntItems =
245+
[
246+
new("1", "1"),
247+
new("12", "12"),
248+
new("123", "123"),
249+
new("1234", "1234")
250+
];
251+
233252
private static Task<bool> OnBeforeSelectedItemChange(SelectedItem item)
234253
{
235254
return Task.FromResult(true);

src/BootstrapBlazor.Server/Locales/en-US.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3111,6 +3111,7 @@
31113111
"SelectsBindingIntro": "The values in the text box change as you change the drop-down option by binding the <code>Model.Name</code> property to the component with <code>Select</code>",
31123112
"SelectsClearableTitle": "Clearable",
31133113
"SelectsClearableIntro": "You can clear Select using a clear icon",
3114+
"SelectsClearableDesc": "Cannot be a null integer. Setting <code>IsClearable</code> has no effect. Its default value is <b>0</b>",
31143115
"SelectsBindingSelectedItemTitle": "Select two-way binding SelectItem type",
31153116
"SelectsBindingSelectedItemIntro": "The values in the text box change as you change the drop-down option by binding the <code>SelectItem</code> property to the component with <code>Select</code> .",
31163117
"SelectsCascadingTitle": "Select cascading binding",

src/BootstrapBlazor.Server/Locales/zh-CN.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3111,6 +3111,7 @@
31113111
"SelectsBindingIntro": "通过 <code>Select</code> 组件绑定 <code>Model.Name</code> 属性,改变下拉框选项时,文本框内的数值随之改变。",
31123112
"SelectsClearableTitle": "可清空单选",
31133113
"SelectsClearableIntro": "包含清空按钮,可将选择器清空为初始状态",
3114+
"SelectsClearableDesc": "不可为空整形设置 <code>IsClearable</code> 无效,其默认值为 <b>0</b>",
31143115
"SelectsBindingSelectedItemTitle": "Select 双向绑定 SelectItem",
31153116
"SelectsBindingSelectedItemIntro": "通过 <code>Select</code> 组件绑定 <code>SelectItem</code> 属性,改变下拉框选项时,文本框内的数值随之改变。",
31163117
"SelectsCascadingTitle": "Select 级联绑定",

src/BootstrapBlazor/Components/Select/Select.razor.cs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public partial class Select<TValue> : ISelect, ILookup
4242
.AddClass($"text-danger", IsValid.HasValue && !IsValid.Value)
4343
.Build();
4444

45-
private bool GetClearable() => IsClearable && !IsDisabled;
45+
private bool GetClearable() => IsClearable && !IsDisabled && IsNullable();
4646

4747
/// <summary>
4848
/// 设置当前项是否 Active 方法
@@ -294,6 +294,11 @@ private SelectedItem? SelectedRow
294294

295295
private SelectedItem? GetSelectedRow()
296296
{
297+
if (Value is null)
298+
{
299+
return null;
300+
}
301+
297302
var item = GetItemWithEnumValue()
298303
?? Rows.Find(i => i.Value == CurrentValueAsString)
299304
?? Rows.Find(i => i.Active)
@@ -393,7 +398,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
393398
/// </summary>
394399
private int TotalCount { get; set; }
395400

396-
private List<SelectedItem> GetVirtualItems() => FilterBySearchText(GetRowsByItems()).ToList();
401+
private List<SelectedItem> GetVirtualItems() => [.. FilterBySearchText(GetRowsByItems())];
397402

398403
/// <summary>
399404
/// 虚拟滚动数据加载回调方法
@@ -539,7 +544,6 @@ private async Task SelectedItemChanged(SelectedItem item)
539544
{
540545
if (_lastSelectedValueString != item.Value)
541546
{
542-
543547
item.Active = true;
544548
SelectedItem = item;
545549

@@ -556,7 +560,7 @@ private async Task SelectedItemChanged(SelectedItem item)
556560
}
557561

558562
/// <summary>
559-
/// 添加静态下拉项方法
563+
/// <inheritdoc/>
560564
/// </summary>
561565
/// <param name="item"></param>
562566
public void Add(SelectedItem item) => _children.Add(item);
@@ -577,22 +581,18 @@ private async Task OnClearValue()
577581
await OnClearAsync();
578582
}
579583

580-
SelectedItem? item;
581584
if (OnQueryAsync != null)
582585
{
583586
await VirtualizeElement.RefreshDataAsync();
584-
item = _result.Items.FirstOrDefault();
585-
}
586-
else
587-
{
588-
item = Items.FirstOrDefault();
589-
}
590-
if (item != null)
591-
{
592-
await SelectedItemChanged(item);
593587
}
588+
589+
_lastSelectedValueString = string.Empty;
590+
CurrentValue = default;
591+
SelectedItem = null;
594592
}
595593

594+
private bool IsNullable() => !ValueType.IsValueType || NullableUnderlyingType != null;
595+
596596
private string? ReadonlyString => IsEditable ? null : "readonly";
597597

598598
private async Task OnChange(ChangeEventArgs args)

test/UnitTest/Components/SelectTest.cs

Lines changed: 100 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Microsoft.AspNetCore.Components.Web.Virtualization;
99
using System.ComponentModel.DataAnnotations;
1010
using System.Reflection;
11+
using System.Runtime.CompilerServices;
1112

1213
namespace UnitTest.Components;
1314

@@ -134,7 +135,7 @@ public void Select_Lookup()
134135
}
135136

136137
[Fact]
137-
public void IsClearable_Ok()
138+
public async Task IsClearable_Ok()
138139
{
139140
var val = "Test2";
140141
var cut = Context.RenderComponent<Select<string>>(pb =>
@@ -154,8 +155,8 @@ public void IsClearable_Ok()
154155
});
155156
});
156157
var clearButton = cut.Find(".clear-icon");
157-
cut.InvokeAsync(() => clearButton.Click());
158-
Assert.Empty(val);
158+
await cut.InvokeAsync(() => clearButton.Click());
159+
Assert.Null(val);
159160

160161
// 提高代码覆盖率
161162
var select = cut;
@@ -174,6 +175,97 @@ public void IsClearable_Ok()
174175
validPi.SetValue(select.Instance, false);
175176
val = pi.GetValue(select.Instance, null)!.ToString();
176177
Assert.Contains("text-danger", val);
178+
179+
// 更改数据类型为不可为空 int
180+
// IsClearable 参数无效
181+
var cut1 = Context.RenderComponent<Select<int>>(pb =>
182+
{
183+
pb.Add(a => a.IsClearable, true);
184+
pb.Add(a => a.Items, new List<SelectedItem>()
185+
{
186+
new("1", "Test1"),
187+
new("2", "Test2"),
188+
new("3", "Test3")
189+
});
190+
pb.Add(a => a.Value, 1);
191+
});
192+
cut1.DoesNotContain("clear-icon");
193+
}
194+
195+
[Fact]
196+
public void IsNullable_Ok()
197+
{
198+
var cut = Context.RenderComponent<Select<string>>(pb =>
199+
{
200+
pb.Add(a => a.Items, new List<SelectedItem>()
201+
{
202+
new("", "请选择"),
203+
new("2", "Test2"),
204+
new("3", "Test3")
205+
});
206+
});
207+
Assert.True(IsNullable(cut.Instance));
208+
209+
var cut1 = Context.RenderComponent<Select<string?>>(pb =>
210+
{
211+
pb.Add(a => a.Items, new List<SelectedItem>()
212+
{
213+
new("", "请选择"),
214+
new("2", "Test2"),
215+
new("3", "Test3")
216+
});
217+
});
218+
Assert.True(IsNullable(cut1.Instance));
219+
220+
var cut2 = Context.RenderComponent<Select<Foo>>(pb =>
221+
{
222+
pb.Add(a => a.Items, new List<SelectedItem>()
223+
{
224+
new("", "请选择"),
225+
new("2", "Test2"),
226+
new("3", "Test3")
227+
});
228+
});
229+
Assert.True(IsNullable(cut2.Instance));
230+
231+
var cut3 = Context.RenderComponent<Select<Foo?>>(pb =>
232+
{
233+
pb.Add(a => a.Items, new List<SelectedItem>()
234+
{
235+
new("", "请选择"),
236+
new("2", "Test2"),
237+
new("3", "Test3")
238+
});
239+
});
240+
Assert.True(IsNullable(cut3.Instance));
241+
242+
var cut4 = Context.RenderComponent<Select<int>>(pb =>
243+
{
244+
pb.Add(a => a.Items, new List<SelectedItem>()
245+
{
246+
new("", "请选择"),
247+
new("2", "Test2"),
248+
new("3", "Test3")
249+
});
250+
});
251+
Assert.False(IsNullable(cut4.Instance));
252+
253+
var cut5 = Context.RenderComponent<Select<int?>>(pb =>
254+
{
255+
pb.Add(a => a.Items, new List<SelectedItem>()
256+
{
257+
new("", "请选择"),
258+
new("2", "Test2"),
259+
new("3", "Test3")
260+
});
261+
});
262+
Assert.True(IsNullable(cut5.Instance));
263+
}
264+
265+
private static bool IsNullable(object select)
266+
{
267+
var mi = select.GetType().GetMethod("IsNullable", BindingFlags.Instance | BindingFlags.NonPublic)!;
268+
return (bool)mi.Invoke(select, null)!;
177269
}
178270

179271
[Fact]
@@ -434,8 +526,7 @@ public void NullBool_Ok()
434526
});
435527

436528
// 值为 null
437-
// 候选项中无,导致默认选择第一个 Value 被更改为 true
438-
Assert.True(cut.Instance.Value);
529+
Assert.Null(cut.Instance.Value);
439530
}
440531

441532
[Fact]
@@ -735,8 +826,8 @@ public async Task IsVirtualize_Items_Clearable_Ok()
735826
var button = cut.Find(".clear-icon");
736827
await cut.InvokeAsync(() => button.Click());
737828

738-
// UI 恢复 Test1
739-
Assert.Equal("Test1", el.Value);
829+
// 可为空数据类型 UI 为 ""
830+
Assert.Equal("", el.Value);
740831

741832
// 下拉框显示所有选项
742833
items = cut.FindAll(".dropdown-item");
@@ -794,8 +885,8 @@ public async Task IsVirtualize_OnQueryAsync_Clearable_Ok()
794885
var button = cut.Find(".clear-icon");
795886
await cut.InvokeAsync(() => button.Click());
796887

797-
// UI 恢复 Test1
798-
Assert.Equal("All", el.Value);
888+
// 可为空数据类型 UI 为 ""
889+
Assert.Equal("", el.Value);
799890

800891
// 下拉框显示所有选项
801892
Assert.True(query);

0 commit comments

Comments
 (0)