Skip to content

Commit 17d5dba

Browse files
ArgoZhangice6
andauthored
feat(Search): OnSearch callback support async (#5000)
* refactor: 增加弃用标记 * refactor: 更改为泛型组件 * doc: 更新文档 * refactor: 移除可为空 * refactor: 使用 ItemTemplate 参数 * doc: 更新示例文档 * test: 更新单元测试 * doc: 更新示例 * test: 更新单元测试 Co-Authored-By: ice6 <[email protected]>
1 parent 7c5b136 commit 17d5dba

File tree

10 files changed

+98
-36
lines changed

10 files changed

+98
-36
lines changed

src/BootstrapBlazor.Server/Components/Pages/Coms.razor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ public sealed partial class Coms
1414

1515
private string? SearchText { get; set; }
1616

17-
private Task<IEnumerable<string>> OnSearch(string searchText)
17+
private Task<IEnumerable<string?>> OnSearch(string searchText)
1818
{
1919
SearchText = searchText;
20-
return Task.FromResult<IEnumerable<string>>(ComponentItems.Where(i => i.Contains(searchText, StringComparison.OrdinalIgnoreCase)));
20+
return Task.FromResult<IEnumerable<string?>>(ComponentItems.Where(i => i.Contains(searchText, StringComparison.OrdinalIgnoreCase)).ToList());
2121
}
2222
}

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
@((MarkupString)@Localizer["NormalDesc"].Value)
1111
</section>
1212
<AutoFill @bind-Value="Model1" Items="Items1" IsLikeMatch="true" OnGetDisplayText="OnGetDisplayText" class="mb-3" IsSelectAllTextOnFocus="true">
13-
<Template>
13+
<ItemTemplate>
1414
<div class="d-flex">
1515
<div>
1616
<img src="@WebsiteOption.CurrentValue.GetAvatarUrl(context.Id)" class="bb-avatar" />
@@ -20,7 +20,7 @@
2020
<div class="bb-sub">@Foo.GetTitle(context.Id)</div>
2121
</div>
2222
</div>
23-
</Template>
23+
</ItemTemplate>
2424
</AutoFill>
2525
<section ignore>
2626
<img src="@WebsiteOption.CurrentValue.GetAvatarUrl(Model1.Id)" class="shadow" style="width: 140px; margin-bottom: 1rem; border-radius: 6px;" />
@@ -32,7 +32,7 @@
3232
<section ignore>@((MarkupString)Localizer["CustomFilterDesc"].Value)</section>
3333
<AutoFill @bind-Value="Model2" Items="Items2" OnCustomFilter="OnCustomFilter"
3434
OnGetDisplayText="OnGetDisplayText" class="mb-3">
35-
<Template>
35+
<ItemTemplate>
3636
<div class="d-flex">
3737
<div>
3838
<img src="@WebsiteOption.CurrentValue.GetAvatarUrl(context.Id)" class="bb-avatar" />
@@ -42,7 +42,7 @@
4242
<div class="bb-sub">@Foo.GetTitle(context.Id)</div>
4343
</div>
4444
</div>
45-
</Template>
45+
</ItemTemplate>
4646
</AutoFill>
4747
<section ignore>
4848
<EditorForm Model="@Model2" RowType="RowType.Inline" ItemsPerRow="2" />
@@ -54,7 +54,7 @@
5454
@((MarkupString)@Localizer["ShowDropdownListOnFocusDesc"].Value)
5555
</section>
5656
<AutoFill @bind-Value="Model3" Items="Items3" ShowDropdownListOnFocus="false" OnGetDisplayText="OnGetDisplayText" class="mb-3">
57-
<Template>
57+
<ItemTemplate>
5858
<div class="d-flex">
5959
<div>
6060
<img src="@WebsiteOption.CurrentValue.GetAvatarUrl(context.Id)" class="bb-avatar" />
@@ -64,7 +64,7 @@
6464
<div class="bb-sub">@Foo.GetTitle(context.Id)</div>
6565
</div>
6666
</div>
67-
</Template>
67+
</ItemTemplate>
6868
</AutoFill>
6969
<section ignore>
7070
<EditorForm Model="@Model3" RowType="RowType.Inline" ItemsPerRow="2" />

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
<BootstrapInputGroup>
9090
<BootstrapInputGroupLabel DisplayText="AutoFill" />
9191
<AutoFill TValue="Foo" Items="AufoFillItems" IsLikeMatch="true" OnGetDisplayText="@(foo => foo.Name ?? "")">
92-
<Template>
92+
<ItemTemplate>
9393
<div class="d-flex">
9494
<div>
9595
<img src="@WebsiteOption.CurrentValue.GetAvatarUrl(context.Id)" class="bb-avatar" />
@@ -99,7 +99,7 @@
9999
<div class="bb-sub">@Foo.GetTitle(context.Id)</div>
100100
</div>
101101
</div>
102-
</Template>
102+
</ItemTemplate>
103103
</AutoFill>
104104
</BootstrapInputGroup>
105105
</div>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@
3535
<ConsoleLogger @ref="DisplayLogger" />
3636
</DemoBlock>
3737

38+
<DemoBlock Title="@Localizer["SearchesItemTemplateTitle"]"
39+
Introduction="@Localizer["SearchesItemTemplateIntro"]"
40+
Name="ItemTemplate">
41+
<Search PlaceHolder="@Localizer["SearchesPlaceHolder"]"
42+
OnSearch="@OnSearchFoo">
43+
<ItemTemplate>
44+
<div>@context.Name</div>
45+
<div>@context.Address</div>
46+
</ItemTemplate>
47+
</Search>
48+
</DemoBlock>
49+
3850
<DemoBlock Title="@Localizer["SearchesKeyboardsTitle"]"
3951
Introduction="@Localizer["SearchesKeyboardsIntro"]"
4052
Name="keyboards">

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ private Task<IEnumerable<string>> OnKeyboardSearch(string searchText)
5454

5555
private Foo Model { get; set; } = new Foo() { Name = "" };
5656

57+
private static async Task<IEnumerable<Foo>> OnSearchFoo(string searchText)
58+
{
59+
await Task.Delay(100);
60+
return Enumerable.Range(1, 10).Select(i => new Foo() { Name = $"{searchText}-{i}", Address = $"Address - 10{i}" }).ToList();
61+
}
62+
5763
/// <summary>
5864
/// 获得属性方法
5965
/// </summary>

src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,14 @@ public partial class AutoFill<TValue>
7070
[Parameter]
7171
public Func<string, Task<IEnumerable<TValue>>>? OnCustomFilter { get; set; }
7272

73+
/// <summary>
74+
/// 获得/设置 候选项模板 默认 null
75+
/// </summary>
76+
[Parameter]
77+
[Obsolete("已弃用,请使用 ItemTemplate 代替;Deprecated please use ItemTemplate parameter")]
78+
[ExcludeFromCodeCoverage]
79+
public RenderFragment<TValue>? Template { get => ItemTemplate; set => ItemTemplate = value; }
80+
7381
[Inject]
7482
[NotNull]
7583
private IStringLocalizer<AutoComplete>? Localizer { get; set; }

src/BootstrapBlazor/Components/Search/Search.razor

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
@namespace BootstrapBlazor.Components
2-
@inherits PopoverCompleteBase<string>
2+
@typeparam TValue
3+
@inherits PopoverCompleteBase<TValue>
34

45
<div @attributes="@AdditionalAttributes" class="@ClassString" id="@Id">
56
<div class="input-group">
@@ -27,7 +28,7 @@
2728
}
2829
else
2930
{
30-
<div>@item</div>
31+
<div>@GetDisplayText(item)</div>
3132
}
3233
</li>
3334
}

src/BootstrapBlazor/Components/Search/Search.razor.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace BootstrapBlazor.Components;
1010
/// <summary>
1111
/// Search 组件
1212
/// </summary>
13-
public partial class Search
13+
public partial class Search<TValue>
1414
{
1515
/// <summary>
1616
/// 获得/设置 是否显示清除按钮 默认为 false 不显示
@@ -77,17 +77,24 @@ public partial class Search
7777
/// 获得/设置 点击搜索按钮时回调委托
7878
/// </summary>
7979
[Parameter]
80-
public Func<string, Task<IEnumerable<string>>>? OnSearch { get; set; }
80+
public Func<string, Task<IEnumerable<TValue>>>? OnSearch { get; set; }
81+
82+
/// <summary>
83+
/// 获得/设置 通过模型获得显示文本方法 默认使用 ToString 重载方法
84+
/// </summary>
85+
[Parameter]
86+
[NotNull]
87+
public Func<TValue, string?>? OnGetDisplayText { get; set; }
8188

8289
/// <summary>
8390
/// 获得/设置 点击清空按钮时回调委托
8491
/// </summary>
8592
[Parameter]
86-
public Func<string, Task>? OnClear { get; set; }
93+
public Func<string?, Task>? OnClear { get; set; }
8794

8895
[Inject]
8996
[NotNull]
90-
private IStringLocalizer<Search>? Localizer { get; set; }
97+
private IStringLocalizer<Search<TValue>>? Localizer { get; set; }
9198

9299
/// <summary>
93100
/// <inheritdoc/>
@@ -105,7 +112,7 @@ public partial class Search
105112
/// 获得/设置 UI 呈现数据集合
106113
/// </summary>
107114
[NotNull]
108-
private List<string>? FilterItems { get; set; }
115+
private List<TValue>? FilterItems { get; set; }
109116

110117
/// <summary>
111118
/// <inheritdoc/>
@@ -139,6 +146,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
139146
}
140147
}
141148

149+
private string _displayText = "";
142150
private bool _show;
143151
/// <summary>
144152
/// 点击搜索按钮时触发此方法
@@ -151,12 +159,12 @@ private async Task OnSearchClick()
151159
ButtonIcon = SearchButtonLoadingIcon;
152160
await Task.Yield();
153161

154-
var items = await OnSearch(Value);
162+
var items = await OnSearch(_displayText);
155163
FilterItems = items.ToList();
156164
ButtonIcon = SearchButtonIcon;
157165
if (IsAutoClearAfterSearch)
158166
{
159-
Value = "";
167+
_displayText = "";
160168
}
161169
if (IsOnInputTrigger == false)
162170
{
@@ -174,12 +182,22 @@ private async Task OnClearClick()
174182
{
175183
if (OnClear != null)
176184
{
177-
await OnClear(Value);
185+
await OnClear(_displayText);
178186
}
179-
CurrentValue = "";
187+
_displayText = "";
180188
FilterItems = [];
181189
}
182190

191+
private string? GetDisplayText(TValue item)
192+
{
193+
var displayText = item?.ToString();
194+
if (OnGetDisplayText != null)
195+
{
196+
displayText = OnGetDisplayText(item);
197+
}
198+
return displayText;
199+
}
200+
183201
/// <summary>
184202
/// TriggerOnChange 方法
185203
/// </summary>
@@ -188,7 +206,7 @@ private async Task OnClearClick()
188206
[JSInvokable]
189207
public async Task TriggerOnChange(string val, bool search = true)
190208
{
191-
CurrentValue = val;
209+
_displayText = val;
192210

193211
if (search)
194212
{

test/UnitTest/Components/AutoFillTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ public void OnGetDisplayText_Ok()
163163
{
164164
pb.Add(a => a.Value, Model);
165165
pb.Add(a => a.Items, Items);
166-
pb.Add(a => a.OnGetDisplayText, foo => foo.Name ?? "");
166+
pb.Add(a => a.OnGetDisplayText, foo => foo.Name);
167167
});
168168
var input = cut.Find("input");
169169
Assert.Equal("张三 1000", input.Attributes["value"]?.Value);

test/UnitTest/Components/SearchTest.cs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ public class SearchTest : BootstrapBlazorTestBase
1010
[Fact]
1111
public void Items_Ok()
1212
{
13-
var cut = Context.RenderComponent<Search>();
13+
var cut = Context.RenderComponent<Search<string>>();
1414
Assert.Contains("<div class=\"search auto-complete\"", cut.Markup);
1515
var menus = cut.FindAll(".dropdown-item");
1616
Assert.Single(menus);
@@ -26,12 +26,12 @@ public void Items_Ok()
2626
[Fact]
2727
public async Task ItemTemplate_Ok()
2828
{
29-
var items = new List<string>() { "test1", "test2" };
30-
var cut = Context.RenderComponent<Search>(pb =>
29+
var items = new List<Foo>() { new() { Name = "test1", Address = "Address 1" }, new() { Name = "test2", Address = "Address 2" } };
30+
var cut = Context.RenderComponent<Search<Foo>>(pb =>
3131
{
3232
pb.Add(a => a.ItemTemplate, item => builder =>
3333
{
34-
builder.AddContent(0, $"Template-{item}");
34+
builder.AddContent(0, $"Template-{item.Name}-{item.Address}");
3535
});
3636
pb.Add(a => a.OnSearch, async v =>
3737
{
@@ -43,14 +43,35 @@ public async Task ItemTemplate_Ok()
4343
await cut.InvokeAsync(() => cut.Instance.TriggerOnChange("t"));
4444
await Task.Delay(20);
4545

46-
Assert.Contains("Template-test1", cut.Markup);
47-
Assert.Contains("Template-test2", cut.Markup);
46+
Assert.Contains("Template-test1-Address 1", cut.Markup);
47+
Assert.Contains("Template-test2-Address 2", cut.Markup);
48+
}
49+
50+
[Fact]
51+
public async Task OnGetDisplayText_Ok()
52+
{
53+
var items = new List<Foo?>() { null, new() { Name = "test1", Address = "Address 1" }, new() { Name = "test2", Address = "Address 2" } };
54+
var cut = Context.RenderComponent<Search<Foo?>>(pb =>
55+
{
56+
pb.Add(a => a.OnSearch, async v =>
57+
{
58+
await Task.Delay(1);
59+
return items;
60+
});
61+
pb.Add(a => a.OnGetDisplayText, foo => foo?.Name);
62+
});
63+
64+
await cut.InvokeAsync(() => cut.Instance.TriggerOnChange("t"));
65+
await Task.Delay(20);
66+
67+
Assert.Contains("test1", cut.Markup);
68+
Assert.Contains("test2", cut.Markup);
4869
}
4970

5071
[Fact]
5172
public void IsOnInputTrigger_Ok()
5273
{
53-
var cut = Context.RenderComponent<Search>(builder =>
74+
var cut = Context.RenderComponent<Search<string>>(builder =>
5475
{
5576
builder.Add(s => s.IsOnInputTrigger, true);
5677
});
@@ -65,7 +86,7 @@ public async Task OnSearchClick_Ok()
6586
{
6687
string? val = null;
6788
var items = new List<string>() { "test1", "test2" };
68-
var cut = Context.RenderComponent<Search>(builder =>
89+
var cut = Context.RenderComponent<Search<string>>(builder =>
6990
{
7091
builder.Add(s => s.SearchButtonIcon, "fa-fw fa-solid fa-magnifying-glass");
7192
builder.Add(s => s.SearchButtonText, "SearchText");
@@ -82,10 +103,6 @@ public async Task OnSearchClick_Ok()
82103
var menus = cut.FindAll(".dropdown-item");
83104
Assert.Single(menus);
84105

85-
await cut.InvokeAsync(() => cut.Instance.TriggerOnChange("test"));
86-
Assert.Equal("test", val);
87-
Assert.Empty(cut.Instance.Value);
88-
89106
var button = cut.Find(".fa-magnifying-glass");
90107
await cut.InvokeAsync(() => button.Click());
91108
await Task.Delay(10);
@@ -98,7 +115,7 @@ public async Task OnSearchClick_Ok()
98115
public async Task OnClearClick_Ok()
99116
{
100117
var ret = false;
101-
var cut = Context.RenderComponent<Search>(builder =>
118+
var cut = Context.RenderComponent<Search<string>>(builder =>
102119
{
103120
builder.Add(s => s.Value, "1");
104121
builder.Add(s => s.ShowClearButton, true);

0 commit comments

Comments
 (0)