@using System.Timers @implements IDisposable @inject IJSRuntime js @if(errorTimer.Enabled) { @("Error!") } else if(isLoading) { @(LoadingText + "...") } else { @Text } @code { [Parameter] public Func OnClick { get; set; } [Parameter] public string Text{ get; set; } [Parameter] public string LoadingText { get; set; } private Dictionary 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(); }