Skip to content

Commit 575f399

Browse files
ArgoZhangice6
andauthored
feat(Cascader): add IsClearable parameter (#5084)
* feat: add IsClearable parameter * refactor: 更新代码 * doc: 代码重构 * refactor: 增加 cls 样式 * style: 增加禁用样式 * doc: 增加 IsClearable 参数文档 * doc: 增加文档 * test: 增加单元测试 * chore: bump version 9.2.7-beta03 Co-Authored-By: ice6 <[email protected]> --------- Co-authored-by: ice6 <[email protected]>
1 parent ed880e5 commit 575f399

File tree

10 files changed

+128
-7
lines changed

10 files changed

+128
-7
lines changed

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,12 @@
6060
</DemoBlock>
6161

6262
<DemoBlock Title="@Localizer["Block3Title"]" Introduction="@Localizer["Block3Intro"]" Name="Bind">
63+
<section ignore>
64+
@((MarkupString)Localizer["Block3Desc"].Value)
65+
</section>
6366
<div class="row g-3">
6467
<div class="col-12 col-sm-6">
65-
<Cascader Color="Color.Primary" Items="@_items" @bind-Value="@Value" />
68+
<Cascader Color="Color.Primary" Items="@_items" @bind-Value="@Value" IsClearable="true" />
6669
</div>
6770
<div class="col-12 col-sm-6">
6871
<BootstrapInput readonly @bind-Value="@Value" />

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
<section ignore>
1111
@((MarkupString)Localizer["NormalDesc"].Value)
1212
</section>
13-
<SelectObject @bind-Value="_value" GetTextCallback="GetTextCallback" IsClearable>
13+
<SelectObject @bind-Value="_value" GetTextCallback="GetTextCallback" IsClearable="true">
1414
<ListView TItem="ListViews.Product" Items="@Products" OnListViewItemClick="item => OnListViewItemClick(item, context)">
1515
<BodyTemplate Context="value">
1616
<Card>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,7 @@
22842284
"Block2Intro": "Cascading selection is not available",
22852285
"Block3Title": "Two-way binding",
22862286
"Block3Intro": "The values in the text box change when you change the cascading selection option by binding the <code>Value</code> property with the <code>Select</code> component.",
2287+
"Block3Desc": "You can use <code>IsClearable</code> to control whether to display the clear button. The default value is <code>false</code>",
22872288
"Block4Title": "Client validation",
22882289
"Block4Intro": "When cascading selection is not selected, click the submit button to block.",
22892290
"Block5Title": "The binding generic is the Guid structure",

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2284,6 +2284,7 @@
22842284
"Block2Intro": "级联选择不可用状态",
22852285
"Block3Title": "Cascader 双向绑定",
22862286
"Block3Intro": "通过 <code>Select</code> 组件绑定 <code>Value</code> 属性,改变级联选择选项时,文本框内的数值随之改变。",
2287+
"Block3Desc": "通过设置 <code>Clearable=\"true\"</code> 参数,使组件获得焦点或者鼠标悬浮时显示一个 <b>清除</b> 小按钮",
22872288
"Block4Title": "Cascader 客户端验证",
22882289
"Block4Intro": "级联选择未选择时,点击提交按钮时拦截。",
22892290
"Block5Title": "绑定泛型为 Guid 结构",

src/BootstrapBlazor/BootstrapBlazor.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk.Razor">
22

33
<PropertyGroup>
4-
<Version>9.2.7-beta02</Version>
4+
<Version>9.2.7-beta03</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/Cascader/Cascader.razor

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString" tabindex="-1">
1010
<input type="text" id="@InputId" readonly disabled="@Disabled" class="@InputClassName" data-bs-toggle="dropdown" placeholder="@PlaceHolder" value="@DisplayTextString" @onblur="OnBlur" />
1111
<span class="@AppendClassName"><i class="@Icon"></i></span>
12+
@if (GetClearable())
13+
{
14+
<span class="@ClearClassString" @onclick="OnClearValue"><i class="@ClearIcon"></i></span>
15+
}
1216
<div class="dropdown-menu shadow">
1317
<CascadingValue Value="SelectedItems">
1418
@foreach (var item in Items)

src/BootstrapBlazor/Components/Cascader/Cascader.razor.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,25 @@ public partial class Cascader<TValue>
7878
[Parameter]
7979
public string? SubMenuIcon { get; set; }
8080

81+
/// <summary>
82+
/// 获得/设置 是否可清除 默认 false
83+
/// </summary>
84+
[Parameter]
85+
public bool IsClearable { get; set; }
86+
87+
/// <summary>
88+
/// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up
89+
/// </summary>
90+
[Parameter]
91+
[NotNull]
92+
public string? ClearIcon { get; set; }
93+
94+
/// <summary>
95+
/// 获得/设置 清除文本内容 OnClear 回调方法 默认 null
96+
/// </summary>
97+
[Parameter]
98+
public Func<Task>? OnClearAsync { get; set; }
99+
81100
/// <summary>
82101
/// 获得/设置 失去焦点回调方法 默认 null
83102
/// </summary>
@@ -98,6 +117,12 @@ public partial class Cascader<TValue>
98117
.AddClass(SubMenuIcon, !string.IsNullOrEmpty(SubMenuIcon))
99118
.Build();
100119

120+
private string? ClearClassString => CssBuilder.Default("clear-icon")
121+
.AddClass($"text-{Color.ToDescriptionString()}", Color != Color.None)
122+
.AddClass($"text-success", IsValid.HasValue && IsValid.Value)
123+
.AddClass($"text-danger", IsValid.HasValue && !IsValid.Value)
124+
.Build();
125+
101126
/// <summary>
102127
/// OnParametersSet 方法
103128
/// </summary>
@@ -107,6 +132,7 @@ protected override void OnParametersSet()
107132

108133
Icon ??= IconTheme.GetIconByKey(ComponentIcons.CascaderIcon);
109134
SubMenuIcon ??= IconTheme.GetIconByKey(ComponentIcons.CascaderSubMenuIcon);
135+
ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectClearIcon);
110136

111137
Items ??= [];
112138

@@ -178,6 +204,7 @@ private void SetDefaultValue(string defaultValue)
178204

179205
private string? ClassString => CssBuilder.Default("select cascade menu dropdown")
180206
.AddClass("disabled", IsDisabled)
207+
.AddClass("cls", IsClearable)
181208
.AddClass(CssClass).AddClass(ValidCss)
182209
.Build();
183210

@@ -196,6 +223,8 @@ private void SetDefaultValue(string defaultValue)
196223
.AddClass($"text-{Color.ToDescriptionString()}", Color != Color.None && !IsDisabled)
197224
.Build();
198225

226+
private bool GetClearable() => IsClearable && !IsDisabled;
227+
199228
/// <summary>
200229
/// 选择项是否 Active 方法
201230
/// </summary>
@@ -252,4 +281,14 @@ private static void SetSelectedNodeWithParent(CascaderItem? item, List<CascaderI
252281
list.Add(item);
253282
}
254283
}
284+
285+
private async Task OnClearValue()
286+
{
287+
if (OnClearAsync != null)
288+
{
289+
await OnClearAsync();
290+
}
291+
292+
CurrentValue = default;
293+
}
255294
}

src/BootstrapBlazor/Components/Select/Select.razor.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
display: flex;
166166
}
167167

168-
&.cls:hover .form-select-append {
168+
&.cls:not(.disabled):hover .form-select-append {
169169
display: none;
170170
}
171171
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ private async Task OnClearValue()
207207
await OnClearAsync();
208208
}
209209

210-
Value = default;
210+
CurrentValue = default;
211211
await CloseAsync();
212212
}
213213
}

test/UnitTest/Components/CascaderTest.cs

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,68 @@ namespace UnitTest.Components;
88
public class CascaderTest : BootstrapBlazorTestBase
99
{
1010
[Fact]
11-
public void ValidateForm_OK()
11+
public async Task ValidateForm_OK()
1212
{
13-
var foo = new Foo();
13+
var foo = new Foo() { Name = "test1" };
14+
var valid = false;
15+
var invalid = false;
16+
var items = new List<CascaderItem>()
17+
{
18+
new() { Text = "Test1", Value = "test1" },
19+
new() { Text = "Test2", Value = "test2" }
20+
};
1421
var cut = Context.RenderComponent<ValidateForm>(pb =>
1522
{
23+
pb.Add(a => a.OnValidSubmit, context =>
24+
{
25+
valid = true;
26+
return Task.CompletedTask;
27+
});
28+
pb.Add(a => a.OnInvalidSubmit, context =>
29+
{
30+
invalid = true;
31+
return Task.CompletedTask;
32+
});
1633
pb.Add(a => a.Model, foo);
1734
pb.AddChildContent<Cascader<string>>(pb =>
1835
{
36+
pb.Add(a => a.Items, items);
1937
pb.Add(a => a.DisplayText, "Test_DisplayText");
2038
pb.Add(a => a.ShowLabel, true);
39+
pb.Add(a => a.IsClearable, true);
40+
pb.Add(a => a.Value, foo.Name);
41+
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Name", typeof(string)));
42+
pb.Add(a => a.OnValueChanged, v =>
43+
{
44+
foo.Name = v;
45+
return Task.CompletedTask;
46+
});
2147
});
2248
});
2349
cut.Contains("Test_DisplayText");
50+
51+
await cut.InvokeAsync(() =>
52+
{
53+
var form = cut.Find("form");
54+
form.Submit();
55+
});
56+
Assert.True(valid);
57+
58+
var span = cut.Find(".clear-icon");
59+
Assert.True(span.ClassList.Contains("text-success"));
60+
61+
foo.Name = null;
62+
var cascader = cut.FindComponent<Cascader<string>>();
63+
cascader.SetParametersAndRender();
64+
await cut.InvokeAsync(() =>
65+
{
66+
var form = cut.Find("form");
67+
form.Submit();
68+
});
69+
Assert.True(invalid);
70+
71+
span = cut.Find(".clear-icon");
72+
Assert.True(span.ClassList.Contains("text-danger"));
2473
}
2574

2675
[Fact]
@@ -29,8 +78,12 @@ public void Color_Ok()
2978
var cut = Context.RenderComponent<Cascader<string>>(pb =>
3079
{
3180
pb.Add(a => a.Color, Color.Success);
81+
pb.Add(a => a.IsClearable, true);
3282
});
3383
cut.Contains("border-success");
84+
85+
var span = cut.Find(".clear-icon");
86+
Assert.True(span.ClassList.Contains("text-success"));
3487
}
3588

3689
[Fact]
@@ -129,12 +182,32 @@ public void IsDisabled_Ok()
129182
var cut = Context.RenderComponent<Cascader<string>>(pb =>
130183
{
131184
pb.Add(a => a.IsDisabled, true);
185+
pb.Add(a => a.IsClearable, true);
132186
});
133187

134188
var input = cut.Find(".dropdown > input");
135189
Assert.True(input.HasAttribute("disabled"));
136190
}
137191

192+
[Fact]
193+
public async Task IsClearable_Ok()
194+
{
195+
var isClear = false;
196+
var cut = Context.RenderComponent<Cascader<string>>(pb =>
197+
{
198+
pb.Add(a => a.IsClearable, true);
199+
pb.Add(a => a.OnClearAsync, () =>
200+
{
201+
isClear = true;
202+
return Task.CompletedTask;
203+
});
204+
});
205+
206+
var clearButton = cut.Find(".clear-icon");
207+
await cut.InvokeAsync(() => clearButton.Click());
208+
Assert.True(isClear);
209+
}
210+
138211
[Fact]
139212
public void SubCascader_NullItems()
140213
{

0 commit comments

Comments
 (0)