Skip to content

Commit 3392e25

Browse files
authored
feat(blazorui): add LoadMore feature to BitBasicList #10996 (#11015)
1 parent 7f09775 commit 3392e25

File tree

13 files changed

+991
-541
lines changed

13 files changed

+991
-541
lines changed

src/BlazorUI/Bit.BlazorUI.Tests/Components/Lists/BasicList/BitBasicListTest.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<BitBasicList Items="Items" EnableVirtualization="Virtualize"
1+
<BitBasicList Items="Items" Virtualize="Virtualize"
22
Role="@Role" Style="@ListStyle" ItemSize="ItemSize" OverscanCount="OverscanCount">
33
<RowTemplate Context="person">
44
<div class="list-item" style="height:@($"{ItemSize}px")">

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.razor

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,45 +4,70 @@
44

55
<div @ref="RootElement" @attributes="HtmlAttributes"
66
id="@_Id"
7+
role="@Role"
78
style="@StyleBuilder.Value"
89
class="@ClassBuilder.Value"
9-
dir="@Dir?.ToString().ToLower()"
10-
role="@Role">
11-
12-
@if (EnableVirtualization)
10+
dir="@Dir?.ToString().ToLower()">
11+
@if (RowTemplate is not null)
1312
{
14-
if (ItemsProvider is null)
13+
if (Virtualize)
1514
{
16-
<_BitBasicListVirtualize Items="Items"
17-
ItemSize="@ItemSize"
18-
OverscanCount="@OverscanCount"
19-
EmptyContent="EmptyContent">
20-
@RowTemplate(context)
21-
</_BitBasicListVirtualize>
15+
if (ItemsProvider is null || LoadMore)
16+
{
17+
<_BitBasicListVirtualize Items="_viewItems"
18+
ItemSize="@ItemSize"
19+
EmptyContent="EmptyContent"
20+
OverscanCount="@OverscanCount">
21+
@RowTemplate(context)
22+
</_BitBasicListVirtualize>
23+
}
24+
else
25+
{
26+
<_BitBasicListVirtualize @ref="_bitBasicListVirtualizeRef"
27+
EmptyContent="EmptyContent"
28+
OverscanCount="@OverscanCount"
29+
Placeholder="VirtualizePlaceholder"
30+
ItemsProvider="ProvideVirtualizedItems">
31+
@RowTemplate(context)
32+
</_BitBasicListVirtualize>
33+
}
2234
}
2335
else
2436
{
25-
<_BitBasicListVirtualize @ref="_bitBasicListVirtualizeRef"
26-
OverscanCount="@OverscanCount"
27-
Placeholder="VirtualizePlaceholder"
28-
ItemsProvider="ProvideVirtualizedItems"
29-
EmptyContent="EmptyContent">
30-
@RowTemplate(context)
31-
</_BitBasicListVirtualize>
37+
if (_viewItems.Count > 0)
38+
{
39+
foreach (var item in _viewItems)
40+
{
41+
@RowTemplate(item)
42+
}
43+
}
44+
else if (_isLoadingMore is false)
45+
{
46+
@EmptyContent
47+
}
3248
}
3349
}
34-
else
50+
51+
@if (LoadMore && _loadMoreFinished is false)
3552
{
36-
if (Items.Count > 0)
37-
{
38-
foreach (var item in Items)
53+
<button class="bit-bsl-lmb" @onclick="() => PerformLoadMore(false)">
54+
@if (LoadMoreTemplate is not null)
3955
{
40-
@RowTemplate(item)
56+
@LoadMoreTemplate(_isLoadingMore)
4157
}
42-
}
43-
else
44-
{
45-
@EmptyContent
46-
}
58+
else
59+
{
60+
<div class="bit-bsl-lmt">
61+
@if (_isLoadingMore)
62+
{
63+
<span>...</span>
64+
}
65+
else
66+
{
67+
<span>@LoadMoreText</span>
68+
}
69+
</div>
70+
}
71+
</button>
4772
}
4873
</div>

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.razor.cs

Lines changed: 148 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,24 @@
55
/// </summary>
66
public partial class BitBasicList<TItem> : BitComponentBase
77
{
8+
private int _loadMoreSkip = 0;
9+
private bool _loadMoreFinished;
10+
private ICollection<TItem> _viewItems = [];
11+
private CancellationTokenSource? _globalCts;
12+
private ICollection<TItem>? _internalItems = null;
813
private _BitBasicListVirtualize<TItem>? _bitBasicListVirtualizeRef;
14+
private BitBasicListItemsProvider<TItem>? _internalItemsProvider = null;
915

1016

1117

12-
/// <summary>
13-
/// The custom content that gets rendered when there is no item to show.
14-
/// </summary>
15-
[Parameter] public RenderFragment? EmptyContent { get; set; }
18+
private bool _isLoadingMore => _globalCts is not null;
19+
20+
1621

1722
/// <summary>
18-
/// Enables virtualization in rendering the list.
23+
/// The custom content that will be rendered when there is no item to show.
1924
/// </summary>
20-
[Parameter] public bool EnableVirtualization { get; set; }
25+
[Parameter] public RenderFragment? EmptyContent { get; set; }
2126

2227
/// <summary>
2328
/// Sets the height of the list to fit its content.
@@ -56,44 +61,72 @@ public partial class BitBasicList<TItem> : BitComponentBase
5661
public bool FullWidth { get; set; }
5762

5863
/// <summary>
59-
/// Gets or sets the list of items to render.
64+
/// The list of items to render.
6065
/// </summary>
61-
[Parameter] public ICollection<TItem> Items { get; set; } = Array.Empty<TItem>();
66+
[Parameter] public ICollection<TItem>? Items { get; set; }
6267

6368
/// <summary>
64-
/// Gets the size of each item in pixels. Defaults to 50px.
69+
/// Size of each item in pixels. Defaults to 50px.
6570
/// </summary>
6671
[Parameter] public float ItemSize { get; set; } = 50f;
6772

6873
/// <summary>
69-
/// Gets or sets a value that determines how many additional items will be rendered before and after the visible region.
74+
/// The function providing items to the list.
75+
/// </summary>
76+
[Parameter] public BitBasicListItemsProvider<TItem>? ItemsProvider { get; set; }
77+
78+
/// <summary>
79+
/// Enables the LoadMore mode for the list.
80+
/// </summary>
81+
[Parameter] public bool LoadMore { get; set; }
82+
83+
/// <summary>
84+
/// The number of items to be loaded and rendered after the LoadMore button is clicked. Defaults to 20.
85+
/// </summary>
86+
[Parameter] public int LoadMoreSize { get; set; } = 20;
87+
88+
/// <summary>
89+
/// The template of the LoadMore button.
90+
/// </summary>
91+
[Parameter] public RenderFragment<bool>? LoadMoreTemplate { get; set; }
92+
93+
/// <summary>
94+
/// The custom text of the default LoadMore button. Defaults to "LoadMore".
95+
/// </summary>
96+
[Parameter] public string? LoadMoreText { get; set; } = "LoadMore";
97+
98+
/// <summary>
99+
/// A value that determines how many additional items will be rendered before and after the visible region in Virtualize mode.
70100
/// </summary>
71101
[Parameter] public int OverscanCount { get; set; } = 3;
72102

73103
/// <summary>
74-
/// Gets or set the role attribute of the BasicList html element.
104+
/// The role attribute of the html element of the list.
75105
/// </summary>
76106
[Parameter] public string Role { get; set; } = "list";
77107

78108
/// <summary>
79-
/// Gets or sets the Template to render each row.
109+
/// The template to render each row.
80110
/// </summary>
81-
[Parameter] public RenderFragment<TItem> RowTemplate { get; set; } = default!;
111+
[Parameter] public RenderFragment<TItem>? RowTemplate { get; set; }
82112

83113
/// <summary>
84-
/// The function providing items to the list
114+
/// Enables virtualization in rendering the list.
85115
/// </summary>
86-
[Parameter] public BitBasicListItemsProvider<TItem>? ItemsProvider { get; set; }
116+
[Parameter] public bool Virtualize { get; set; }
87117

88118
/// <summary>
89-
/// The template for items that have not yet been loaded in memory.
119+
/// The template for items that have not yet rendered.
90120
/// </summary>
91121
[Parameter] public RenderFragment<PlaceholderContext>? VirtualizePlaceholder { get; set; }
92122

93123

94124

95125
public async Task RefreshDataAsync()
96126
{
127+
_globalCts?.Cancel();
128+
_globalCts = null;
129+
97130
if (ItemsProvider is null) return;
98131
if (_bitBasicListVirtualizeRef is null) return;
99132

@@ -114,8 +147,91 @@ protected override void RegisterCssStyles()
114147
StyleBuilder.Register(() => (FitSize || FitHeight) ? "height:fit-content" : string.Empty);
115148
}
116149

150+
protected override async Task OnParametersSetAsync()
151+
{
152+
if (_internalItems != Items)
153+
{
154+
_internalItems = Items;
155+
156+
if (ItemsProvider is null) // ItemsProvider always has priority over Items
157+
{
158+
_viewItems = Items ?? [];
159+
160+
if (LoadMore)
161+
{
162+
await PerformLoadMore(true);
163+
}
164+
}
165+
}
166+
167+
if (_internalItemsProvider != ItemsProvider)
168+
{
169+
_internalItemsProvider = ItemsProvider;
170+
171+
if (LoadMore && ItemsProvider is not null)
172+
{
173+
await PerformLoadMore(true);
174+
}
175+
}
176+
177+
await base.OnParametersSetAsync();
178+
}
179+
180+
181+
private async Task PerformLoadMore(bool reset)
182+
{
183+
if (reset)
184+
{
185+
_viewItems = [];
186+
_loadMoreSkip = 0;
187+
_loadMoreFinished = false;
188+
}
189+
190+
if (LoadMore is false || _globalCts is not null) return;
191+
192+
var localCts = new CancellationTokenSource();
193+
_globalCts = localCts;
194+
195+
try
196+
{
197+
StateHasChanged();
198+
199+
try
200+
{
201+
if (ItemsProvider is null)
202+
{
203+
var items = Items ?? [];
204+
205+
_viewItems = [.. _viewItems, .. items.Skip(_loadMoreSkip).Take(LoadMoreSize)];
206+
207+
_loadMoreFinished = _viewItems.Count >= items.Count;
208+
}
209+
else
210+
{
211+
var result = await ProvideVirtualizedItems(new(_loadMoreSkip, LoadMoreSize, localCts.Token));
212+
213+
if (localCts.IsCancellationRequested is false)
214+
{
215+
_viewItems = [.. _viewItems, .. result.Items];
216+
217+
//_loadMoreFinished = _viewItems.Count >= result.TotalItemCount; // for performance purposes we won't use TotalItemCount here!
218+
_loadMoreFinished = result.Items.Any() is false;
219+
}
220+
}
221+
222+
_loadMoreSkip += LoadMoreSize;
223+
}
224+
catch (OperationCanceledException oce) when (oce.CancellationToken == localCts.Token) { }
225+
}
226+
finally
227+
{
228+
_globalCts = null;
229+
localCts.Dispose();
230+
}
231+
232+
StateHasChanged();
233+
}
117234

118-
// Gets called both by RefreshDataCoreAsync and directly by the Virtualize child component during scrolling
119235
private async ValueTask<ItemsProviderResult<TItem>> ProvideVirtualizedItems(ItemsProviderRequest request)
120236
{
121237
if (ItemsProvider is null) return default;
@@ -133,4 +249,19 @@ private async ValueTask<ItemsProviderResult<TItem>> ProvideVirtualizedItems(Item
133249

134250
return new ItemsProviderResult<TItem>(providerResult.Items, providerResult.TotalItemCount);
135251
}
252+
253+
254+
255+
protected override async ValueTask DisposeAsync(bool disposing)
256+
{
257+
if (IsDisposed || disposing is false) return;
258+
259+
if (_globalCts is not null)
260+
{
261+
_globalCts.Dispose();
262+
_globalCts = null;
263+
}
264+
265+
await base.DisposeAsync(disposing);
266+
}
136267
}

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicList.scss

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,17 @@
33
.bit-bsl {
44
overflow: auto;
55
height: spacing(62.5);
6-
}
6+
}
7+
8+
.bit-bsl-lmb {
9+
display: contents;
10+
}
11+
12+
.bit-bsl-lmt {
13+
width: 100%;
14+
display: flex;
15+
cursor: pointer;
16+
padding: spacing(2);
17+
align-items: center;
18+
justify-content: center;
19+
}

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/ItemsProvider/BitBasicListItemsProvider.cs renamed to src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicListItemsProvider.cs

File renamed without changes.

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/ItemsProvider/BitBasicListItemsProviderRequest.cs renamed to src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicListItemsProviderRequest.cs

File renamed without changes.

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/ItemsProvider/BitBasicListItemsProviderResult.cs renamed to src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/BitBasicListItemsProviderResult.cs

File renamed without changes.

src/BlazorUI/Bit.BlazorUI/Components/Lists/BasicList/_BitBasicListVirtualize.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -35,16 +35,16 @@ protected override void BuildRenderTree(RenderTreeBuilder builder)
3535
{
3636
var seq = 0;
3737
builder.OpenComponent<Virtualize<TItem>>(seq++);
38-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.Items), Items);
39-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.ItemSize), ItemSize);
40-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.Placeholder), Placeholder);
41-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.ItemsProvider), ItemsProvider);
42-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.OverscanCount), OverscanCount);
38+
builder.AddAttribute(seq++, nameof(Virtualize<>.Items), Items);
39+
builder.AddAttribute(seq++, nameof(Virtualize<>.ItemSize), ItemSize);
40+
builder.AddAttribute(seq++, nameof(Virtualize<>.Placeholder), Placeholder);
41+
builder.AddAttribute(seq++, nameof(Virtualize<>.ItemsProvider), ItemsProvider);
42+
builder.AddAttribute(seq++, nameof(Virtualize<>.OverscanCount), OverscanCount);
4343

44-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.ItemContent),
45-
(RenderFragment<TItem>)(item => b => b.AddContent(seq++, (ItemContent ?? ChildContent)?.Invoke(item))));
44+
builder.AddAttribute(seq++, nameof(Virtualize<>.ItemContent),
45+
(RenderFragment<TItem>)(item => b => b.AddContent(seq++, (ItemContent ?? ChildContent)?.Invoke(item))));
4646

47-
builder.AddAttribute(seq++, nameof(Virtualize<TItem>.EmptyContent), (RenderFragment)(b => b.AddContent(seq++, EmptyContent)));
47+
builder.AddAttribute(seq++, nameof(Virtualize<>.EmptyContent), (RenderFragment)(b => b.AddContent(seq++, EmptyContent)));
4848

4949
builder.AddComponentReferenceCapture(seq++, v => _virtualizeRef = (Virtualize<TItem>)v);
5050

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Bit.BlazorUI;
2+
3+
[AttributeUsage(AttributeTargets.All)]
4+
internal class CallOnSetAsyncAttribute(string name) : Attribute
5+
{
6+
public string Name { get; set; } = name;
7+
}

0 commit comments

Comments
 (0)