summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Pages/Component/LinkAction.razor102
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();
+}