Skip to content

Commit 0b24a25

Browse files
j4587698ArgoZhang
andauthored
feat(Select): support SelectedItem<TValue> generic value (#4512)
* AutoFill改造Select高亮 * refactor: 格式化代码 * Revert "AutoFill改造Select高亮" This reverts commit b728968. # Conflicts: # src/BootstrapBlazor/Components/AutoFill/AutoFill.razor.cs * feat: 增加 IModelEqualityComparer 接口 * feat: 增加编辑逻辑 * doc: 更新示例 * doc: 更新示例 * test: 增加 OnChanged 单元测试 * test: 增加 OnSelectedItemChanged 单元测试 * test: 增加单元测试 * test: 更新单元测试 * test: 增加 TextConvertToValueCallback 单元测试 * doc: 增加可编辑功能文档 * doc: 更新文档 --------- Co-authored-by: Argo-AsicoTech <[email protected]>
1 parent e94371a commit 0b24a25

File tree

9 files changed

+276
-23
lines changed

9 files changed

+276
-23
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -386,17 +386,18 @@
386386
</DemoBlock>
387387

388388
<DemoBlock Title="@Localizer["SelectsIsEditableTitle"]" Introduction="@Localizer["SelectsIsEditableIntro"]" Name="IsEditable">
389+
<section ignore>@((MarkupString)Localizer["SelectsIsEditableDesc"].Value)</section>
389390
<div class="row">
390391
<div class="col-12 col-sm-6">
391-
<Select TValue="string" Color="Color.Primary" Items="Items" IsEditable="true"></Select>
392+
<Select TValue="string" Color="Color.Primary" Items="Items" IsEditable="true" OnInputChangedCallback="OnInputChangedCallback"></Select>
392393
</div>
393394
</div>
394395
</DemoBlock>
395396

396397
<DemoBlock Title="@Localizer["SelectsVirtualizeTitle"]"
397398
Introduction="@Localizer["SelectsVirtualizeIntro"]"
398399
Name="IsVirtualize">
399-
<p>@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)</p>
400+
<section ignore>@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)</section>
400401

401402
<div class="row mb-3">
402403
<div class="col-12 col-sm-6">
@@ -434,6 +435,20 @@
434435
</div>
435436
</DemoBlock>
436437

438+
<DemoBlock Title="@Localizer["SelectsGenericTitle"]"
439+
Introduction="@Localizer["SelectsGenericIntro"]"
440+
Name="Generic">
441+
<section ignore>@((MarkupString)Localizer["SelectsGenericDesc"].Value)</section>
442+
<div class="row">
443+
<div class="col-12 col-sm-6">
444+
<Select Items="_genericItems" @bind-Value="_selectedFoo" IsEditable="true"></Select>
445+
</div>
446+
<div class="col-12 col-sm-6">
447+
<Display Value="_selectedFoo?.Address"></Display>
448+
</div>
449+
</div>
450+
</DemoBlock>
451+
437452
<AttributeTable Items="@GetAttributes()" />
438453

439454
<EventTable Items="@GetEvents()" />

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

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ private async Task OnCascadeBindSelectClick(SelectedItem item)
141141
}
142142
else
143143
{
144-
Items2 = Enumerable.Empty<SelectedItem>();
144+
Items2 = [];
145145
}
146146
StateHasChanged();
147147
}
@@ -174,6 +174,19 @@ private async Task OnCascadeBindSelectClick(SelectedItem item)
174174

175175
private int? NullableSelectedIntItem { get; set; }
176176

177+
private Task OnInputChangedCallback(string v)
178+
{
179+
var item = Items.FirstOrDefault(i => i.Text.Equals(v, StringComparison.OrdinalIgnoreCase));
180+
if (item == null)
181+
{
182+
item = new SelectedItem() { Value = v, Text = v };
183+
var items = Items.ToList();
184+
items.Insert(0, item);
185+
Items = items;
186+
}
187+
return Task.CompletedTask;
188+
}
189+
177190
private string GetSelectedIntItemString()
178191
{
179192
return NullableSelectedIntItem.HasValue ? NullableSelectedIntItem.Value.ToString() : "null";
@@ -234,6 +247,15 @@ private Task OnTimeZoneValueChanged(string timeZoneId)
234247
return Task.CompletedTask;
235248
}
236249

250+
private readonly List<SelectedItem<Foo>> _genericItems =
251+
[
252+
new() { Text = "Foo1", Value = new Foo() { Id = 1, Address = "Address_F001" } },
253+
new() { Text = "Foo2", Value = new Foo() { Id = 2, Address = "Address_F002" } },
254+
new() { Text = "Foo3", Value = new Foo() { Id = 3, Address = "Address_F003" } }
255+
];
256+
257+
private Foo? _selectedFoo;
258+
237259
/// <summary>
238260
/// 获得事件方法
239261
/// </summary>
@@ -251,6 +273,18 @@ private EventItem[] GetEvents() =>
251273
Name = "OnBeforeSelectedItemChange",
252274
Description = Localizer["SelectsOnBeforeSelectedItemChange"],
253275
Type = "Func<SelectedItem, Task<bool>>"
276+
},
277+
new()
278+
{
279+
Name = "OnInputChangedCallback",
280+
Description = Localizer["SelectsOnInputChangedCallback"],
281+
Type = "Func<string, Task>"
282+
},
283+
new()
284+
{
285+
Name = "TextConvertToValueCallback",
286+
Description = Localizer["SelectsTextConvertToValueCallback"],
287+
Type = "Func<string, Task<TValue>>"
254288
}
255289
];
256290

@@ -301,6 +335,14 @@ private AttributeItem[] GetAttributes() =>
301335
DefaultValue = "Primary"
302336
},
303337
new()
338+
{
339+
Name = "IsEditable",
340+
Description = Localizer["SelectsIsEditable"],
341+
Type = "boolean",
342+
ValueList = "true / false",
343+
DefaultValue = "false"
344+
},
345+
new()
304346
{
305347
Name = "IsDisabled",
306348
Description = Localizer["SelectsIsDisabled"],

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3177,9 +3177,16 @@
31773177
"SelectsPopoverIntro": "Set <code>IsPopover</code> to <b>true</b>, use <code>popover</code> render UI prevent The dropdown menu cannot be fully displayed because the parent container is set to <code>overflow: hidden</code>",
31783178
"SelectsIsEditableTitle": "Editable",
31793179
"SelectsIsEditableIntro": "By setting <code>IsEditable=\"true\"</code> to make the component editable",
3180+
"SelectsIsEditableDesc": "After the editable function is enabled, if the input value is not in the candidate items, the new value can be returned through the <code>TextConvertToValueCallback</code> callback method, and the <code>Items</code> data source can be updated through the <code>OnInputChangedCallback</code> callback to prevent the input value from being lost after the page is refreshed.",
31803181
"SelectsVirtualizeTitle": "Virtualize",
31813182
"SelectsVirtualizeIntro": "Set <code>IsVirtualize</code> to <b>true</b> enable virtual scroll for larg data",
3182-
"SelectsVirtualizeDescription": "Component virtual scrolling supports two ways of providing data through <code>Items</code> or <code>OnQueryAsync</code> callback methods"
3183+
"SelectsVirtualizeDescription": "Component virtual scrolling supports two ways of providing data through <code>Items</code> or <code>OnQueryAsync</code> callback methods",
3184+
"SelectsGenericTitle": "Generic",
3185+
"SelectsGenericIntro": "Data source <code>Items</code> supports generics when using <code>SelectedItem&lt;TValue&gt;</code>",
3186+
"SelectsGenericDesc": "<p>Please refer to <a href=\"https://github.com/dotnetcore/BootstrapBlazor/issues/4497?wt.mc_id=DT-MVP-5004174\" target=\"_blank\">Design Ideas</a> to understand this feature. In this example, by selecting the drop-down box option, the value obtained is the <code>Foo</code> instance, and the value displayed in the text box on the right is the <code>Address</code> value of the <code>Foo</code> attribute</p><p>In this example, the <code>ValueEqualityComparer</code> and <code>CustomKeyAttribute</code> parameters are not set, and the <code>[Key]</code> tag of the <code>Id</code> attribute of <code>Foo</code> is used for equality judgment</p>",
3187+
"SelectsOnInputChangedCallback": "Callback method for converting input text into corresponding Value in edit mode",
3188+
"TextConvertToValueCallback": "Callback method when input text changes in edit mode",
3189+
"SelectsIsEditable": "Whether editable"
31833190
},
31843191
"BootstrapBlazor.Server.Components.Samples.Sliders": {
31853192
"SlidersTitle": "Slider",

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3177,9 +3177,16 @@
31773177
"SelectsPopoverIntro": "通过设置 <code>IsPopover</code> 参数,组件使用 <code>popover</code> 渲染 <code>UI</code> 防止由于父容器设置 <code>overflow: hidden;</code> 使弹窗无法显示问题",
31783178
"SelectsIsEditableTitle": "可编辑",
31793179
"SelectsIsEditableIntro": "通过设置 <code>IsEditable=\"true\"</code> 使组件可录入",
3180+
"SelectsIsEditableDesc": "开启可编辑功能后,输入值如果候选项中没有时,可以通过 <code>TextConvertToValueCallback</code> 回调方法返回新值,可以通过 <code>OnInputChangedCallback</code> 回调对 <code>Items</code> 数据源进行更新,防止页面刷新后输入值丢失",
31803181
"SelectsVirtualizeTitle": "虚拟滚动",
31813182
"SelectsVirtualizeIntro": "通过设置 <code>IsVirtualize</code> 参数开启组件虚拟功能特性",
3182-
"SelectsVirtualizeDescription": "组件虚拟滚动支持两种形式通过 <code>Items</code> 或者 <code>OnQueryAsync</code> 回调方法提供数据"
3183+
"SelectsVirtualizeDescription": "组件虚拟滚动支持两种形式通过 <code>Items</code> 或者 <code>OnQueryAsync</code> 回调方法提供数据",
3184+
"SelectsGenericTitle": "泛型支持",
3185+
"SelectsGenericIntro": "数据源 <code>Items</code> 使用 <code>SelectedItem&lt;TValue&gt;</code> 时即可支持泛型",
3186+
"SelectsGenericDesc": "<p>请参考 <a href=\"https://github.com/dotnetcore/BootstrapBlazor/issues/4497?wt.mc_id=DT-MVP-5004174\" target=\"_blank\">设计思路</a> 理解此功能。本例中通过选择下拉框选项,得到的值为 <code>Foo</code> 实例,右侧文本框内显示值为 <code>Foo</code> 属性 <code>Address</code> 值</p><p>本例中未设置 <code>ValueEqualityComparer</code> 以及 <code>CustomKeyAttribute</code> 参数,使用 <code>Foo</code> 属性 <code>Id</code> 的 <code>[Key]</code> 标签进行相等判定</p>",
3187+
"SelectsOnInputChangedCallback": "编辑模式下输入文本转换为对应 Value 回调方法",
3188+
"TextConvertToValueCallback": "编辑模式下输入文本变化时回调方法",
3189+
"SelectsIsEditable": "是否可编辑"
31833190
},
31843191
"BootstrapBlazor.Server.Components.Samples.Sliders": {
31853192
"SlidersTitle": "Slider 滑块",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ protected override void OnInitialized()
121121

122122
NoDataTip ??= Localizer[nameof(NoDataTip)];
123123
PlaceHolder ??= Localizer[nameof(PlaceHolder)];
124-
Items ??= Enumerable.Empty<TValue>();
124+
Items ??= [];
125125
}
126126

127127
/// <summary>

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

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace BootstrapBlazor.Components;
1212
/// Select 组件实现类
1313
/// </summary>
1414
/// <typeparam name="TValue"></typeparam>
15-
public partial class Select<TValue> : ISelect
15+
public partial class Select<TValue> : ISelect, IModelEqualityComparer<TValue>
1616
{
1717
[Inject]
1818
[NotNull]
@@ -50,7 +50,7 @@ public partial class Select<TValue> : ISelect
5050
/// <param name="item"></param>
5151
/// <returns></returns>
5252
private string? ActiveItem(SelectedItem item) => CssBuilder.Default("dropdown-item")
53-
.AddClass("active", () => item.Value == CurrentValueAsString)
53+
.AddClass("active", Match(item))
5454
.AddClass("disabled", item.IsDisabled)
5555
.Build();
5656

@@ -95,6 +95,13 @@ public partial class Select<TValue> : ISelect
9595
[Parameter]
9696
public Func<string, Task>? OnInputChangedCallback { get; set; }
9797

98+
/// <summary>
99+
/// 获得/设置 选项输入更新后转换为 Value 回调方法 默认 null
100+
/// </summary>
101+
/// <remarks>设置 <see cref="IsEditable"/> 后生效</remarks>
102+
[Parameter]
103+
public Func<string, Task<TValue>>? TextConvertToValueCallback { get; set; }
104+
98105
/// <summary>
99106
/// 获得/设置 无搜索结果时显示文字
100107
/// </summary>
@@ -164,6 +171,26 @@ public partial class Select<TValue> : ISelect
164171
[Parameter]
165172
public bool DisableItemChangedWhenFirstRender { get; set; }
166173

174+
/// <summary>
175+
/// 获得/设置 比较数据是否相同回调方法 默认为 null
176+
/// <para>提供此回调方法时忽略 <see cref="CustomKeyAttribute"/> 属性</para>
177+
/// </summary>
178+
[Parameter]
179+
public Func<TValue, TValue, bool>? ValueEqualityComparer { get; set; }
180+
181+
Func<TValue, TValue, bool>? IModelEqualityComparer<TValue>.ModelEqualityComparer
182+
{
183+
get => ValueEqualityComparer;
184+
set => ValueEqualityComparer = value;
185+
}
186+
187+
/// <summary>
188+
/// 获得/设置 数据主键标识标签 默认为 <see cref="KeyAttribute"/>用于判断数据主键标签,如果模型未设置主键时可使用 <see cref="ValueEqualityComparer"/> 参数自定义判断数据模型支持联合主键
189+
/// </summary>
190+
[Parameter]
191+
[NotNull]
192+
public Type? CustomKeyAttribute { get; set; } = typeof(KeyAttribute);
193+
167194
[NotNull]
168195
private Virtualize<SelectedItem>? VirtualizeElement { get; set; }
169196

@@ -300,7 +327,7 @@ private void ResetSelectedItem()
300327
_dataSource.AddRange(VirtualItems);
301328
}
302329

303-
SelectedItem = _dataSource.Find(i => i.Value.Equals(CurrentValueAsString, StringComparison))
330+
SelectedItem = _dataSource.Find(Match)
304331
?? _dataSource.Find(i => i.Active)
305332
?? _dataSource.Where(i => !i.IsDisabled).FirstOrDefault()
306333
?? GetVirtualizeItem();
@@ -337,6 +364,8 @@ private void ResetSelectedItem()
337364
/// <returns></returns>
338365
protected override Task InvokeInitAsync() => InvokeVoidAsync("init", Id, Interop, nameof(ConfirmSelectedItem));
339366

367+
private bool Match(SelectedItem i) => i is SelectedItem<TValue> d ? Equals(d.Value, Value) : i.Value.Equals(CurrentValueAsString, StringComparison);
368+
340369
/// <summary>
341370
/// 客户端回车回调方法
342371
/// </summary>
@@ -391,6 +420,27 @@ private async Task OnClickItem(SelectedItem item)
391420
}
392421

393422
private async Task SelectedItemChanged(SelectedItem item)
423+
{
424+
if (item is SelectedItem<TValue> d && !Equals(d.Value, Value))
425+
{
426+
item.Active = true;
427+
SelectedItem = item;
428+
429+
CurrentValue = d.Value;
430+
431+
// 触发 SelectedItemChanged 事件
432+
if (OnSelectedItemChanged != null)
433+
{
434+
await OnSelectedItemChanged(SelectedItem);
435+
}
436+
}
437+
else
438+
{
439+
await ValueTypeChanged(item);
440+
}
441+
}
442+
443+
private async Task ValueTypeChanged(SelectedItem item)
394444
{
395445
if (_lastSelectedValueString != item.Value)
396446
{
@@ -463,21 +513,54 @@ private async Task OnChange(ChangeEventArgs args)
463513
{
464514
if (args.Value is string v)
465515
{
516+
// 判断是否为泛型 SelectedItem
517+
var isGeneric = Items.GetType().GetGenericArguments().Length > 0;
518+
466519
// Items 中没有时插入一个 SelectedItem
467-
if (Items.FirstOrDefault(i => i.Text == v) == null)
520+
var item = Items.FirstOrDefault(i => i.Text == v);
521+
522+
TValue? val = default;
523+
if (item == null)
468524
{
469-
var items = new List<SelectedItem>
525+
if (isGeneric)
470526
{
471-
new(v, v)
472-
};
527+
if (TextConvertToValueCallback != null)
528+
{
529+
val = await TextConvertToValueCallback(v);
530+
}
531+
item = new SelectedItem<TValue>() { Text = v, Value = val };
532+
}
533+
else
534+
{
535+
item = new SelectedItem(v, v);
536+
}
537+
538+
var items = new List<SelectedItem>() { item };
473539
items.AddRange(Items);
474540
Items = items;
475541
}
542+
543+
if (item is SelectedItem<TValue> value)
544+
{
545+
CurrentValue = value.Value;
546+
}
547+
else
548+
{
549+
CurrentValueAsString = v;
550+
}
551+
476552
if (OnInputChangedCallback != null)
477553
{
478554
await OnInputChangedCallback(v);
479555
}
480-
CurrentValueAsString = v;
481556
}
482557
}
558+
559+
/// <summary>
560+
/// <inheritdoc/>
561+
/// </summary>
562+
/// <param name="x"></param>
563+
/// <param name="y"></param>
564+
/// <returns></returns>
565+
public bool Equals(TValue? x, TValue? y) => this.Equals<TValue>(x, y);
483566
}

src/BootstrapBlazor/Misc/IModelEqualityComparer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ namespace BootstrapBlazor.Components;
1111
public interface IModelEqualityComparer<TItem>
1212
{
1313
/// <summary>
14-
///
14+
/// 获得/设置 模型比对回调方法
1515
/// </summary>
1616
Func<TItem, TItem, bool>? ModelEqualityComparer { get; set; }
1717

1818
/// <summary>
19-
///
19+
/// 获得/设置 模型键值标签
2020
/// </summary>
2121
Type CustomKeyAttribute { get; set; }
2222

2323
/// <summary>
24-
///
24+
/// 相等判定方法
2525
/// </summary>
2626
/// <param name="x"></param>
2727
/// <param name="y"></param>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the Apache 2.0 License
3+
// See the LICENSE file in the project root for more information.
4+
// Maintainer: Argo Zhang([email protected]) Website: https://www.blazor.zone
5+
6+
namespace BootstrapBlazor.Components;
7+
8+
/// <summary>
9+
/// <see cref="SelectedItem"/> 泛型实现类
10+
/// </summary>
11+
public class SelectedItem<T> : SelectedItem
12+
{
13+
/// <summary>
14+
/// 获得/设置 泛型值
15+
/// </summary>
16+
public new T? Value { get; set; }
17+
}

0 commit comments

Comments
 (0)