Skip to content

Commit 366c830

Browse files
authored
feat(MultiSelect): add IsVirtualize parameter (#5652)
* refactor: multi-select 支持虚拟滚动 * doc: 更新示例 * refactor: 增加固定搜索功能 * doc: 更新示例 * refactor: 清除缓存 * refactor: 更新数据初始化逻辑 * doc: 更新示例 * refactor: 增加 cls 样式 * style: 调整样式 * refactor: 移除重复参数 * test: 更新单元测试 * refactor: 更新 Clear 图标 * test: 更新单元测试 * test: 增加单元测试 * test: 增加单元测试 * test: 增加单元测试 * doc: 更新示例 * Revert "refactor: 更新 Clear 图标" This reverts commit eebd3c4. * feat: 增加 ClearableIcon 参数 * test: 增加单元测试 * doc: 更新文档 * doc: 更新文档注释 * style: 更新样式 * refactor: 移动方法到基类 * refactor: 重构 OnClearValue 方法 * test: 更新单元测试 * refactor: 移动到基类 * refactor: 移动 _virtualizeElement 到基类 * refactor: 移动方法到基类 * refactor: 更新虚拟滚动逻辑 * refactor: 移动 IsEditable 到基类 * doc: 增加注释 * refactor: 移动到基类 * refactor: 移动到基类 * refactor: 移动到基类 * refactor: 复用代码 * refactor: 更改为虚类 * refactor: 移除 DefaultVirtualizeItemText 参数 * test: 更新单元测试 * chore: bump version 9.5.0-beta02
1 parent 9f1df2b commit 366c830

21 files changed

+797
-494
lines changed

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

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,18 +123,24 @@ private enum MultiSelectEnumFoo
123123
<Checkbox @bind-Value="@_isFixedSearch" />
124124
</BootstrapInputGroup>
125125
</div>
126+
<div class="col-12 col-sm-6">
127+
<BootstrapInputGroup>
128+
<BootstrapInputGroupLabel DisplayText="IsClearable" />
129+
<Checkbox @bind-Value="@_isClearable" />
130+
</BootstrapInputGroup>
131+
</div>
126132
</div>
127133
</section>
128134

129135
<div class="row g-3">
130136
<div class="col-12 col-sm-6">
131137
<MultiSelect Items="@Items" @bind-Value="@SelectedSearchItemsValue"
132-
ShowSearch="true" IsFixedSearch="_isFixedSearch" OnSearchTextChanged="@OnSearch">
138+
ShowSearch="true" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable" OnSearchTextChanged="@OnSearch">
133139
</MultiSelect>
134140
</div>
135141
<div class="col-12 col-sm-6">
136142
<MultiSelect Items="@LongItems" @bind-Value="@SelectedMaxItemsValue"
137-
ShowSearch="true" IsFixedSearch="_isFixedSearch">
143+
ShowSearch="true" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable">
138144
</MultiSelect>
139145
</div>
140146
</div>
@@ -295,6 +301,58 @@ private enum MultiSelectEnumFoo
295301
</div>
296302
</DemoBlock>
297303

304+
<DemoBlock Title="@Localizer["MultiSelectVirtualizeTitle"]"
305+
Introduction="@Localizer["MultiSelectVirtualizeIntro"]"
306+
Name="IsVirtualize">
307+
<section ignore>
308+
<p>@((MarkupString)Localizer["MultiSelectVirtualizeDescription"].Value)</p>
309+
<div class="row g-3">
310+
<div class="col-12 col-sm-6">
311+
<BootstrapInputGroup>
312+
<BootstrapInputGroupLabel DisplayText="ShowSearch" />
313+
<Checkbox @bind-Value="@_showSearch" />
314+
</BootstrapInputGroup>
315+
</div>
316+
<div class="col-12 col-sm-6">
317+
<BootstrapInputGroup>
318+
<BootstrapInputGroupLabel DisplayText="IsFixedSearch" />
319+
<Checkbox @bind-Value="@_isFixedSearch" />
320+
</BootstrapInputGroup>
321+
</div>
322+
<div class="col-12 col-sm-6">
323+
<BootstrapInputGroup>
324+
<BootstrapInputGroupLabel DisplayText="IsClearable" />
325+
<Checkbox @bind-Value="@_isClearable" />
326+
</BootstrapInputGroup>
327+
</div>
328+
</div>
329+
</section>
330+
331+
<p class="code-label">1. 使用 OnQueryAsync 作为数据源</p>
332+
<div class="row mb-3">
333+
<div class="col-6">
334+
<MultiSelect IsVirtualize="true" @bind-Value="_virtualItemString1" OnQueryAsync="OnQueryAsync"
335+
ShowSearch="_showSearch" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable">
336+
</MultiSelect>
337+
</div>
338+
<div class="col-6">
339+
<Display TValue="string" Value="@_virtualItemString1"></Display>
340+
</div>
341+
</div>
342+
343+
<p class="code-label">2. 使用 Items 作为数据源</p>
344+
<div class="row">
345+
<div class="col-6">
346+
<MultiSelect IsVirtualize="true" @bind-Value="_virtualItemString2" Items="VirtualItems"
347+
ShowSearch="_showSearch" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable">
348+
</MultiSelect>
349+
</div>
350+
<div class="col-6">
351+
<Display TValue="string" Value="@_virtualItemString2"></Display>
352+
</div>
353+
</div>
354+
</DemoBlock>
355+
298356
<AttributeTable Items="@GetAttributes()"></AttributeTable>
299357

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

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

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ namespace BootstrapBlazor.Server.Components.Samples;
1010
/// </summary>
1111
public partial class MultiSelects
1212
{
13+
[Inject]
14+
[NotNull]
15+
private IStringLocalizer<Foo>? LocalizerFoo { get; set; }
16+
1317
/// <summary>
1418
/// Foo 类为Demo测试用,如有需要请自行下载源码查阅
1519
/// Foo class is used for Demo tet, please download the source code if necessary
@@ -106,9 +110,20 @@ private enum MultiSelectEnumFoo
106110

107111
private List<SelectedItem> CascadingItems1 { get; set; } = [];
108112

113+
[NotNull]
114+
private List<Foo>? Foos { get; set; }
115+
116+
private string? _virtualItemString1;
117+
118+
private string? _virtualItemString2;
119+
120+
private IEnumerable<SelectedItem> VirtualItems => Foos.Select(i => new SelectedItem(i.Name!, i.Name!)).ToList();
121+
109122
private string? _editString;
110123

111124
private bool _isFixedSearch;
125+
private bool _isClearable;
126+
private bool _showSearch;
112127

113128
private async Task<SelectedItem> OnEditCallback(string value)
114129
{
@@ -123,6 +138,21 @@ private async Task<SelectedItem> OnEditCallback(string value)
123138
return item;
124139
}
125140

141+
private async Task<QueryData<SelectedItem>> OnQueryAsync(VirtualizeQueryOption option)
142+
{
143+
await Task.Delay(200);
144+
var items = Foos;
145+
if (!string.IsNullOrEmpty(option.SearchText))
146+
{
147+
items = [.. Foos.Where(i => i.Name!.Contains(option.SearchText, StringComparison.OrdinalIgnoreCase))];
148+
}
149+
return new QueryData<SelectedItem>
150+
{
151+
Items = items.Skip(option.StartIndex).Take(option.Count).Select(i => new SelectedItem(i.Name!, i.Name!)),
152+
TotalCount = items.Count
153+
};
154+
}
155+
126156
private SelectedItem[] GroupItems { get; } =
127157
[
128158
new("Jilin", "吉林") { GroupName = "东北"},
@@ -142,7 +172,7 @@ private async Task<SelectedItem> OnEditCallback(string value)
142172
];
143173

144174
/// <summary>
145-
/// OnInitialized
175+
/// <inheritdoc/>
146176
/// </summary>
147177
protected override void OnInitialized()
148178
{
@@ -183,6 +213,9 @@ protected override void OnInitialized()
183213
LongItems = GenerateDataSource(LongDataSource);
184214

185215
Items = GenerateDataSource(DataSource);
216+
Foos = Foo.GenerateFoo(LocalizerFoo);
217+
_virtualItemString1 = Foos[79].Name;
218+
_virtualItemString2 = Foos[45].Name;
186219
}
187220

188221
private static List<SelectedItem> GenerateItems() =>
@@ -198,7 +231,7 @@ private static List<SelectedItem> GenerateItems() =>
198231
new ("Lianyungang", "连云港")
199232
];
200233

201-
private static List<SelectedItem> GenerateDataSource(List<SelectedItem> source) => source.Select(i => new SelectedItem(i.Value, i.Text)).ToList();
234+
private static List<SelectedItem> GenerateDataSource(List<SelectedItem> source) => [.. source.Select(i => new SelectedItem(i.Value, i.Text))];
202235

203236
private void AddItems()
204237
{

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

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -433,13 +433,19 @@
433433
Name="IsVirtualize">
434434
<section ignore>
435435
<p>@((MarkupString)Localizer["SelectsVirtualizeDescription"].Value)</p>
436-
<div class="row mb-3">
436+
<div class="row g-3">
437437
<div class="col-12 col-sm-6">
438438
<BootstrapInputGroup>
439439
<BootstrapInputGroupLabel DisplayText="ShowSearch" />
440440
<Checkbox @bind-Value="@_showSearch" />
441441
</BootstrapInputGroup>
442442
</div>
443+
<div class="col-12 col-sm-6">
444+
<BootstrapInputGroup>
445+
<BootstrapInputGroupLabel DisplayText="IsFixedSearch" />
446+
<Checkbox @bind-Value="@_isFixedSearch" />
447+
</BootstrapInputGroup>
448+
</div>
443449
<div class="col-12 col-sm-6">
444450
<BootstrapInputGroup>
445451
<BootstrapInputGroupLabel DisplayText="IsClearable" />
@@ -452,7 +458,8 @@
452458
<p class="code-label">1. 使用 OnQueryAsync 作为数据源</p>
453459
<div class="row mb-3">
454460
<div class="col-6">
455-
<Select IsVirtualize="true" OnQueryAsync="OnQueryAsync" @bind-Value="VirtualItem1" ShowSearch="_showSearch" IsClearable="_isClearable"></Select>
461+
<Select IsVirtualize="true" OnQueryAsync="OnQueryAsync" @bind-Value="VirtualItem1"
462+
ShowSearch="_showSearch" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable"></Select>
456463
</div>
457464
<div class="col-6">
458465
<Display TValue="string" Value="@VirtualItem1?.Text"></Display>
@@ -462,7 +469,8 @@
462469
<p class="code-label">2. 使用 Items 作为数据源</p>
463470
<div class="row">
464471
<div class="col-6">
465-
<Select IsVirtualize="true" Items="VirtualItems" @bind-Value="VirtualItem2" ShowSearch="_showSearch" IsClearable="_isClearable"></Select>
472+
<Select IsVirtualize="true" Items="VirtualItems" @bind-Value="VirtualItem2"
473+
ShowSearch="_showSearch" IsFixedSearch="_isFixedSearch" IsClearable="_isClearable"></Select>
466474
</div>
467475
<div class="col-6">
468476
<Display TValue="string" Value="@VirtualItem2?.Text"></Display>

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.5.0-beta01</Version>
4+
<Version>9.5.0-beta02</Version>
55
</PropertyGroup>
66

77
<ItemGroup>

src/BootstrapBlazor/Components/Select/MultiSelect.razor

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
@namespace BootstrapBlazor.Components
2+
@using Microsoft.AspNetCore.Components.Web.Virtualization
23
@typeparam TValue
3-
@inherits SelectBase<TValue>
4+
@inherits SimpleSelectBase<TValue>
45
@attribute [BootstrapModuleAutoLoader("Select/MultiSelect.razor.js", JSObjectReference = true)]
56

67
@if (IsShowLabel)
@@ -48,6 +49,10 @@
4849
<span class="@AppendClassString"><i class="@DropdownIcon"></i></span>
4950
}
5051
</div>
52+
@if (GetClearable())
53+
{
54+
<span class="@ClearClassString" @onclick="OnClearValue"><i class="@ClearableIcon"></i></span>
55+
}
5156
<div class="@DropdownMenuClassString">
5257
@if (ShowSearch)
5358
{
@@ -57,7 +62,23 @@
5762
<i class="@SearchLoadingIconString"></i>
5863
</div>
5964
}
60-
@if (Rows.Count == 0)
65+
@if (IsVirtualize)
66+
{
67+
<div class="dropdown-menu-body dropdown-virtual">
68+
@if (OnQueryAsync == null)
69+
{
70+
<Virtualize ItemSize="RowHeight" OverscanCount="OverscanCount" Items="@GetVirtualItems()" ChildContent="RenderRow">
71+
</Virtualize>
72+
}
73+
else
74+
{
75+
<Virtualize ItemSize="RowHeight" OverscanCount="OverscanCount" ItemsProvider="LoadItems"
76+
Placeholder="RenderPlaceHolderRow" ItemContent="RenderRow" @ref="_virtualizeElement">
77+
</Virtualize>
78+
}
79+
</div>
80+
}
81+
else if (Rows.Count == 0)
6182
{
6283
<div class="dropdown-item">@NoSearchDataText</div>
6384
}
@@ -91,28 +112,50 @@
91112
}
92113
@foreach (var item in itemGroup)
93114
{
94-
<DynamicElement OnClick="() => ToggleRow(item.Value)" TriggerClick="@CheckCanTrigger(item)" class="@GetItemClassString(item)">
95-
<div class="multi-select-item">
96-
<div class="form-check">
97-
<input class="form-check-input" type="checkbox" disabled="@CheckCanSelect(item)" checked="@GetCheckedString(item)" />
98-
</div>
99-
@if (ItemTemplate != null)
100-
{
101-
@ItemTemplate(item)
102-
}
103-
else if (IsMarkupString)
104-
{
105-
@((MarkupString)item.Text)
106-
}
107-
else
108-
{
109-
<span>@item.Text</span>
110-
}
111-
</div>
112-
</DynamicElement>
115+
@RenderRow(item)
116+
}
117+
118+
if (!string.IsNullOrEmpty(itemGroup.Key))
119+
{
120+
if (GroupItemTemplate != null)
121+
{
122+
@GroupItemTemplate(itemGroup.Key)
123+
}
124+
else
125+
{
126+
<Divider Text="@itemGroup.Key" />
127+
}
113128
}
114129
}
115130
</div>
116131
}
117132
</div>
118133
</div>
134+
135+
@code {
136+
RenderFragment<SelectedItem> RenderRow => item =>
137+
@<DynamicElement OnClick="() => ToggleRow(item.Value)" TriggerClick="@CheckCanTrigger(item)" class="@GetItemClassString(item)">
138+
<div class="multi-select-item">
139+
<div class="form-check">
140+
<input class="form-check-input" type="checkbox" disabled="@CheckCanSelect(item)" checked="@GetCheckedString(item)" />
141+
</div>
142+
@if (ItemTemplate != null)
143+
{
144+
@ItemTemplate(item)
145+
}
146+
else if (IsMarkupString)
147+
{
148+
@((MarkupString)item.Text)
149+
}
150+
else
151+
{
152+
<span>@item.Text</span>
153+
}
154+
</div>
155+
</DynamicElement>;
156+
157+
RenderFragment<PlaceholderContext> RenderPlaceHolderRow => context =>
158+
@<div class="dropdown-item">
159+
<div class="is-ph"></div>
160+
</div>;
161+
}

0 commit comments

Comments
 (0)