Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading