Skip to content

Commit

Permalink
feat(Cascader): add IsClearable parameter (#5084)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
ArgoZhang and ice6 authored Jan 10, 2025
1 parent ed880e5 commit 575f399
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,12 @@
</DemoBlock>

<DemoBlock Title="@Localizer["Block3Title"]" Introduction="@Localizer["Block3Intro"]" Name="Bind">
<section ignore>
@((MarkupString)Localizer["Block3Desc"].Value)
</section>
<div class="row g-3">
<div class="col-12 col-sm-6">
<Cascader Color="Color.Primary" Items="@_items" @bind-Value="@Value" />
<Cascader Color="Color.Primary" Items="@_items" @bind-Value="@Value" IsClearable="true" />
</div>
<div class="col-12 col-sm-6">
<BootstrapInput readonly @bind-Value="@Value" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<section ignore>
@((MarkupString)Localizer["NormalDesc"].Value)
</section>
<SelectObject @bind-Value="_value" GetTextCallback="GetTextCallback" IsClearable>
<SelectObject @bind-Value="_value" GetTextCallback="GetTextCallback" IsClearable="true">
<ListView TItem="ListViews.Product" Items="@Products" OnListViewItemClick="item => OnListViewItemClick(item, context)">
<BodyTemplate Context="value">
<Card>
Expand Down
1 change: 1 addition & 0 deletions src/BootstrapBlazor.Server/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,7 @@
"Block2Intro": "Cascading selection is not available",
"Block3Title": "Two-way binding",
"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.",
"Block3Desc": "You can use <code>IsClearable</code> to control whether to display the clear button. The default value is <code>false</code>",
"Block4Title": "Client validation",
"Block4Intro": "When cascading selection is not selected, click the submit button to block.",
"Block5Title": "The binding generic is the Guid structure",
Expand Down
1 change: 1 addition & 0 deletions src/BootstrapBlazor.Server/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -2284,6 +2284,7 @@
"Block2Intro": "级联选择不可用状态",
"Block3Title": "Cascader 双向绑定",
"Block3Intro": "通过 <code>Select</code> 组件绑定 <code>Value</code> 属性,改变级联选择选项时,文本框内的数值随之改变。",
"Block3Desc": "通过设置 <code>Clearable=\"true\"</code> 参数,使组件获得焦点或者鼠标悬浮时显示一个 <b>清除</b> 小按钮",
"Block4Title": "Cascader 客户端验证",
"Block4Intro": "级联选择未选择时,点击提交按钮时拦截。",
"Block5Title": "绑定泛型为 Guid 结构",
Expand Down
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/BootstrapBlazor.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<Version>9.2.7-beta02</Version>
<Version>9.2.7-beta03</Version>
</PropertyGroup>

<ItemGroup>
Expand Down
4 changes: 4 additions & 0 deletions src/BootstrapBlazor/Components/Cascader/Cascader.razor
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
<div @attributes="AdditionalAttributes" id="@Id" class="@ClassString" tabindex="-1">
<input type="text" id="@InputId" readonly disabled="@Disabled" class="@InputClassName" data-bs-toggle="dropdown" placeholder="@PlaceHolder" value="@DisplayTextString" @onblur="OnBlur" />
<span class="@AppendClassName"><i class="@Icon"></i></span>
@if (GetClearable())
{
<span class="@ClearClassString" @onclick="OnClearValue"><i class="@ClearIcon"></i></span>
}
<div class="dropdown-menu shadow">
<CascadingValue Value="SelectedItems">
@foreach (var item in Items)
Expand Down
39 changes: 39 additions & 0 deletions src/BootstrapBlazor/Components/Cascader/Cascader.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,25 @@ public partial class Cascader<TValue>
[Parameter]
public string? SubMenuIcon { get; set; }

/// <summary>
/// 获得/设置 是否可清除 默认 false
/// </summary>
[Parameter]
public bool IsClearable { get; set; }

/// <summary>
/// 获得/设置 右侧清除图标 默认 fa-solid fa-angle-up
/// </summary>
[Parameter]
[NotNull]
public string? ClearIcon { get; set; }

/// <summary>
/// 获得/设置 清除文本内容 OnClear 回调方法 默认 null
/// </summary>
[Parameter]
public Func<Task>? OnClearAsync { get; set; }

/// <summary>
/// 获得/设置 失去焦点回调方法 默认 null
/// </summary>
Expand All @@ -98,6 +117,12 @@ public partial class Cascader<TValue>
.AddClass(SubMenuIcon, !string.IsNullOrEmpty(SubMenuIcon))
.Build();

private string? ClearClassString => CssBuilder.Default("clear-icon")
.AddClass($"text-{Color.ToDescriptionString()}", Color != Color.None)
.AddClass($"text-success", IsValid.HasValue && IsValid.Value)
.AddClass($"text-danger", IsValid.HasValue && !IsValid.Value)
.Build();

/// <summary>
/// OnParametersSet 方法
/// </summary>
Expand All @@ -107,6 +132,7 @@ protected override void OnParametersSet()

Icon ??= IconTheme.GetIconByKey(ComponentIcons.CascaderIcon);
SubMenuIcon ??= IconTheme.GetIconByKey(ComponentIcons.CascaderSubMenuIcon);
ClearIcon ??= IconTheme.GetIconByKey(ComponentIcons.SelectClearIcon);

Items ??= [];

Expand Down Expand Up @@ -178,6 +204,7 @@ private void SetDefaultValue(string defaultValue)

private string? ClassString => CssBuilder.Default("select cascade menu dropdown")
.AddClass("disabled", IsDisabled)
.AddClass("cls", IsClearable)
.AddClass(CssClass).AddClass(ValidCss)
.Build();

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

private bool GetClearable() => IsClearable && !IsDisabled;

/// <summary>
/// 选择项是否 Active 方法
/// </summary>
Expand Down Expand Up @@ -252,4 +281,14 @@ private static void SetSelectedNodeWithParent(CascaderItem? item, List<CascaderI
list.Add(item);
}
}

private async Task OnClearValue()
{
if (OnClearAsync != null)
{
await OnClearAsync();
}

CurrentValue = default;
}
}
2 changes: 1 addition & 1 deletion src/BootstrapBlazor/Components/Select/Select.razor.scss
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@
display: flex;
}

&.cls:hover .form-select-append {
&.cls:not(.disabled):hover .form-select-append {
display: none;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ private async Task OnClearValue()
await OnClearAsync();
}

Value = default;
CurrentValue = default;
await CloseAsync();
}
}
77 changes: 75 additions & 2 deletions test/UnitTest/Components/CascaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,68 @@ namespace UnitTest.Components;
public class CascaderTest : BootstrapBlazorTestBase
{
[Fact]
public void ValidateForm_OK()
public async Task ValidateForm_OK()
{
var foo = new Foo();
var foo = new Foo() { Name = "test1" };
var valid = false;
var invalid = false;
var items = new List<CascaderItem>()
{
new() { Text = "Test1", Value = "test1" },
new() { Text = "Test2", Value = "test2" }
};
var cut = Context.RenderComponent<ValidateForm>(pb =>
{
pb.Add(a => a.OnValidSubmit, context =>
{
valid = true;
return Task.CompletedTask;
});
pb.Add(a => a.OnInvalidSubmit, context =>
{
invalid = true;
return Task.CompletedTask;
});
pb.Add(a => a.Model, foo);
pb.AddChildContent<Cascader<string>>(pb =>
{
pb.Add(a => a.Items, items);
pb.Add(a => a.DisplayText, "Test_DisplayText");
pb.Add(a => a.ShowLabel, true);
pb.Add(a => a.IsClearable, true);
pb.Add(a => a.Value, foo.Name);
pb.Add(a => a.ValueExpression, Utility.GenerateValueExpression(foo, "Name", typeof(string)));
pb.Add(a => a.OnValueChanged, v =>
{
foo.Name = v;
return Task.CompletedTask;
});
});
});
cut.Contains("Test_DisplayText");

await cut.InvokeAsync(() =>
{
var form = cut.Find("form");
form.Submit();
});
Assert.True(valid);

var span = cut.Find(".clear-icon");
Assert.True(span.ClassList.Contains("text-success"));

foo.Name = null;
var cascader = cut.FindComponent<Cascader<string>>();
cascader.SetParametersAndRender();
await cut.InvokeAsync(() =>
{
var form = cut.Find("form");
form.Submit();
});
Assert.True(invalid);

span = cut.Find(".clear-icon");
Assert.True(span.ClassList.Contains("text-danger"));
}

[Fact]
Expand All @@ -29,8 +78,12 @@ public void Color_Ok()
var cut = Context.RenderComponent<Cascader<string>>(pb =>
{
pb.Add(a => a.Color, Color.Success);
pb.Add(a => a.IsClearable, true);
});
cut.Contains("border-success");

var span = cut.Find(".clear-icon");
Assert.True(span.ClassList.Contains("text-success"));
}

[Fact]
Expand Down Expand Up @@ -129,12 +182,32 @@ public void IsDisabled_Ok()
var cut = Context.RenderComponent<Cascader<string>>(pb =>
{
pb.Add(a => a.IsDisabled, true);
pb.Add(a => a.IsClearable, true);
});

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

[Fact]
public async Task IsClearable_Ok()
{
var isClear = false;
var cut = Context.RenderComponent<Cascader<string>>(pb =>
{
pb.Add(a => a.IsClearable, true);
pb.Add(a => a.OnClearAsync, () =>
{
isClear = true;
return Task.CompletedTask;
});
});

var clearButton = cut.Find(".clear-icon");
await cut.InvokeAsync(() => clearButton.Click());
Assert.True(isClear);
}

[Fact]
public void SubCascader_NullItems()
{
Expand Down

0 comments on commit 575f399

Please sign in to comment.