diff options
| author | Jake Mannens <jake@asger.xyz> | 2026-06-22 00:25:55 +1000 |
|---|---|---|
| committer | Jake Mannens <jake@asger.xyz> | 2026-06-22 00:35:13 +1000 |
| commit | 703dac9f34a93cf244675a453fe8e49e6a1d7482 (patch) | |
| tree | 0ede85c548c897e1a39fe144b72156449e2a2f19 | |
| parent | c93cd20e31941912d9f000b151e48e6e9fd81638 (diff) | |
Added LinkAction Razor component
| -rw-r--r-- | Pages/Component/LinkAction.razor | 102 |
1 files changed, 102 insertions, 0 deletions
diff --git a/Pages/Component/LinkAction.razor b/Pages/Component/LinkAction.razor new file mode 100644 index 0000000..ee6d189 --- /dev/null +++ b/Pages/Component/LinkAction.razor @@ -0,0 +1,102 @@ +@using System.Timers +@implements IDisposable +@inject IJSRuntime js + +<a + class=@(isLoading || errorTimer.Enabled ? "disabled" : "") + @attributes=Attributes + @onclick=Trigger> + + @if(errorTimer.Enabled) { + @("Error!") + } else if(isLoading) { + @(LoadingText + "...") + } else { + @Text + } +</a> + +@code { + [Parameter] + public Func<Task> OnClick { get; set; } + + [Parameter] + public string Text{ get; set; } + + [Parameter] + public string LoadingText { get; set; } + + private Dictionary<string, object?> Attributes => new() { + ["disabled"] = isLoading, + }; + + private Timer loadingTimer; + private Timer errorTimer; + + private object triggerLock = new(); + private bool isRunning = false; + private bool isLoading = false; + + protected override void OnInitialized() { + loadingTimer = new(50) { + AutoReset = false + }; + loadingTimer.Elapsed += LoadingTimerElapsed; + + errorTimer = new(1000) { + AutoReset = false + }; + errorTimer.Elapsed += ErrorTimerElapsed; + } + + // Most link-click actions (e.g. triggering dialogs, page + // navigation, etc) complete very quickly (under 50ms). + // Triggering a render cycle to show a loading animation is + // usually pointless as doing so would waste resources and + // appear less seemless to the user who will see brief + // flickering while the browser quickly recalculates it's + // layout twice. To that end, a timer is used to only render + // the loading animation on the link if the delegate task + // takes longer than 50ms + private void LoadingTimerElapsed(object? sender, EventArgs e) { + lock(triggerLock) { + if(isRunning) + isLoading = true; + InvokeAsync(() => StateHasChanged()); + } + } + + private void ErrorTimerElapsed(object? sender, EventArgs e) => + InvokeAsync(() => StateHasChanged()); + + private async Task Trigger() { + lock(triggerLock) { + if(isRunning) + return; + isRunning = true; + loadingTimer.Start(); + } + + bool error = false; + + try { + await Task.Run(OnClick); + } catch { + error = true; + } finally { + lock(triggerLock) { + loadingTimer.Stop(); + isRunning = false; + isLoading = false; + } + } + + if(error) + errorTimer.Start(); + + await InvokeAsync(() => StateHasChanged()); + } + + public void Dispose() => + loadingTimer.Dispose(); +} |
