Skip to content

Commit

Permalink
♻ refactor: refactor InfiniteScroll component (#405)
Browse files Browse the repository at this point in the history
  • Loading branch information
capdiem authored May 15, 2023
1 parent 6c61277 commit d1304ff
Show file tree
Hide file tree
Showing 8 changed files with 172 additions and 61 deletions.
18 changes: 14 additions & 4 deletions src/Component/BlazorComponent.Web/src/interop.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import registerDirective from "./directive/index";
import { parseTouchEvent } from "./events/EventType";
import { registerExtraEvents } from "./events/index";
import { parseTouchEvent } from './events/EventType';
import { getDom, getElementSelector } from "./utils/helper";

export function getZIndex(el?: Element | null): number {
Expand Down Expand Up @@ -1264,14 +1264,24 @@ function isWindow(element: any | Window): element is Window {
return element === window
}

export function checkIfThresholdIsExceededWhenScrolling(el: Element, parent: Element, threshold: number) {
export function checkIfThresholdIsExceededWhenScrolling(el: Element, parent: any, threshold: number) {
if (!el || !parent) return

let parentElement: HTMLElement | Window

if (parent == "window") {
parentElement = window;
} else if (parent == "document") {
parentElement = document.documentElement;
} else {
parentElement = document.querySelector(parent);
}

const rect = el.getBoundingClientRect();
const elementTop = rect.top;
const current = isWindow(parent)
const current = isWindow(parentElement)
? window.innerHeight
: parent.getBoundingClientRect().bottom
: parentElement.getBoundingClientRect().bottom

return (current >= elementTop - threshold)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,58 @@
<div class="@CssProvider.GetClass()" style="@CssProvider.GetStyle()" @ref="Ref">
@if (ChildContent != null)
{
@ChildContent((HasMore, _failed, CreateEventCallback(Retry)))
@ChildContent((_loadStatus, CreateEventCallback(DoLoadMore)))
}
else
{
@if (!HasMore)
if (_loadStatus == InfiniteScrollLoadStatus.Empty)
{
<span class="@CssProvider.GetClass("text--no-more")">@NoMoreText</span>
@if (EmptyContent != null)
{
@EmptyContent
}
else
{
<span>@EmptyText</span>
}
}
else if (_failed && !_loading)
else if (_loadStatus == InfiniteScrollLoadStatus.Loading)
{
<span class="@CssProvider.GetClass("text--failed")">@FailedToLoadText</span>
<a @onclick="Retry">@ReloadText</a>
@if (LoadingContent != null)
{
@LoadingContent
}
else
{
<span>@LoadingText</span>
<BProgressCircular Indeterminate Size="24" Color="@Color" @attributes="@GetAttributes(typeof(BProgressCircular))"></BProgressCircular>
}
}
else if (_loadStatus == InfiniteScrollLoadStatus.Error)
{
if (ErrorContent != null)
{
@ErrorContent.Invoke(DoLoadMore)
}
else
{
<span>@ErrorText</span>

<BButton OnClick="DoLoadMore" Color="@Color" @attributes="@GetAttributes(typeof(BButton), "retry")">
<BIcon>$retry</BIcon>
</BButton>
}
}
else
{
<span class="@CssProvider.GetClass("text--loading")">@LoadingText</span>
<BProgressCircular Indeterminate Size="15" Width="2"></BProgressCircular>
@if (LoadMoreContent != null)
{
@LoadMoreContent.Invoke(DoLoadMore)
}
else
{
<BButton OnClick="DoLoadMore" Color="@Color" @attributes="@GetAttributes(typeof(BButton), "loadMore")">@LoadMoreText</BButton>
}
}
}
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -5,108 +5,160 @@ namespace BlazorComponent;
public partial class BInfiniteScroll : BDomComponentBase
{
[Parameter, EditorRequired]
public EventCallback OnLoadMore { get; set; }

[Parameter, EditorRequired]
public bool HasMore { get; set; }
public EventCallback<InfiniteScrollLoadEventArgs> OnLoad { get; set; }

/// <summary>
/// The parent element that has overflow style.
/// </summary>
[Parameter, EditorRequired]
public ElementReference? Parent { get; set; }
public OneOf<ElementReference, string>? Parent { get; set; }

[Parameter]
public string? Color { get; set; }

[Parameter]
public bool Manual
{
get => GetValue<bool>();
set => SetValue(value);
}

[Parameter]
[ApiDefaultValue(250)]
public StringNumber Threshold { get; set; } = 250;

[Parameter]
public RenderFragment<(bool HasMore, bool Failed, EventCallback Retry)>? ChildContent { get; set; }
public RenderFragment<(InfiniteScrollLoadStatus Status, EventCallback OnLoad)>? ChildContent { get; set; }

[Parameter]
public string? NoMoreText { get; set; }
public string? EmptyText { get; set; }

[Parameter]
public string? FailedToLoadText { get; set; }
public string? LoadingText { get; set; }

[Parameter]
public string? LoadingText { get; set; }
public string? LoadMoreText { get; set; }

[Parameter]
public string? ReloadText { get; set; }
public string? ErrorText { get; set; }

[Parameter]
public RenderFragment? EmptyContent { get; set; }

[Parameter]
public RenderFragment<Func<Task>>? ErrorContent { get; set; }

[Parameter]
public RenderFragment? LoadingContent { get; set; }

[Parameter]
public RenderFragment<Func<Task>>? LoadMoreContent { get; set; }

private static readonly SemaphoreSlim s_semaphoreSlim = new(1, 1);

private bool _loading;
private bool _failed;
private bool _isAttached;
private string? _parentSelector;
private InfiniteScrollLoadStatus _loadStatus;

protected override async Task OnParametersSetAsync()
protected override void RegisterWatchers(PropertyWatcher watcher)
{
await base.OnParametersSetAsync();
base.RegisterWatchers(watcher);

watcher.Watch<bool>(nameof(Manual), ManualChangeCallback);
}

if (!_isAttached && Parent?.Id is not null)
private async void ManualChangeCallback(bool val)
{
if (val)
{
_isAttached = true;
await AddScrollListener();
}
}

NextTick(async () =>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);

if (firstRender)
{
await DoLoadMore();

NextTick(async () => { await AddScrollListener(); });
StateHasChanged();
}
}

private async Task AddScrollListener()
{
if (!_isAttached && Parent is { Value: not null })
{
string? selector = null;

if (Parent.Value.IsT0 && Parent.Value.AsT0.Id is not null)
{
selector = Parent.Value.AsT0.GetSelector();
}
else if (Parent.Value.IsT1)
{
selector = Parent.Value.AsT1;
}

if (selector is null)
{
await Js.AddHtmlElementEventListener(Parent.GetSelector()!, "scroll", OnScroll, false, new EventListenerExtras(0, 100));
return;
}

_isAttached = true;
_parentSelector = selector;

// Run manually once to check whether the threshold is exceeded.
// Use NextTick to wait for the list rendering to complete,
// otherwise we will get the wrong top for the first time.
await OnScroll();
});
await Js.AddHtmlElementEventListener(selector, "scroll", OnScroll, false, new EventListenerExtras(0, 100));
}
}

private async Task OnScroll()
{
if (!OnLoadMore.HasDelegate) return;
if (_parentSelector is null || Manual || !OnLoad.HasDelegate) return;

await s_semaphoreSlim.WaitAsync();

if (_failed)
if (_loadStatus == InfiniteScrollLoadStatus.Error)
{
s_semaphoreSlim.Release();
return;
}

// OPTIMIZE: Combine scroll event and the following js interop.
var exceeded = await JsInvokeAsync<bool>(JsInteropConstants.CheckIfThresholdIsExceededWhenScrolling, Ref, Parent, Threshold.ToDouble());
var exceeded = await JsInvokeAsync<bool>(JsInteropConstants.CheckIfThresholdIsExceededWhenScrolling, Ref, _parentSelector,
Threshold.ToDouble());
if (!exceeded)
{
s_semaphoreSlim.Release();
return;
}

await DoLoadMore();
StateHasChanged();

s_semaphoreSlim.Release();
}

private async Task DoLoadMore()
{
_loadStatus = InfiniteScrollLoadStatus.Loading;

var eventArgs = new InfiniteScrollLoadEventArgs();

try
{
_failed = false;
await OnLoadMore.InvokeAsync();
await OnLoad.InvokeAsync(eventArgs);
_loadStatus = eventArgs.Status;
}
catch (Exception e)
{
_loadStatus = InfiniteScrollLoadStatus.Error;

Logger.LogWarning(e, "Failed to load more");
_failed = true;
StateHasChanged();
}
}

private async Task Retry()
{
_loading = true;

await DoLoadMore();

_loading = false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
namespace BlazorComponent;

public class InfiniteScrollLoadEventArgs : EventArgs
{
public InfiniteScrollLoadStatus Status { get; set; }
}

public enum InfiniteScrollLoadStatus
{
Ok,
Error,
Empty,
Loading
}
8 changes: 4 additions & 4 deletions src/Component/BlazorComponent/I18n/Locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,10 @@
"pleaseSelect": "Please select"
},
"infiniteScroll": {
"noMore": "There's no more",
"failedToLoad": "Failed to load",
"reload": "Reload",
"loading": "Loading"
"emptyText": "There's no more",
"errorText": "Failed to load",
"loadingText": "Loading...",
"loadMoreText": "Load more"
}
}
}
10 changes: 5 additions & 5 deletions src/Component/BlazorComponent/I18n/Locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,10 @@
"pleaseSelect": "请选择"
},
"infiniteScroll": {
"noMore": "没有更多了",
"failedToLoad": "加载失败",
"reload": "重新加载",
"loading": "加载中"
},
"emptyText": "没有更多了",
"errorText": "加载失败",
"loadingText": "加载中...",
"loadMoreText": "加载更多"
}
}
}

Large diffs are not rendered by default.

Large diffs are not rendered by default.

0 comments on commit d1304ff

Please sign in to comment.