@using System.Timers @implements IDisposable @inject IJSRuntime js @code { [Parameter] public RenderFragment? ChildContent { get; set; } [Parameter] public Func OnClick { get; set; } [Parameter] public Color BackgroundColor { get; set; } = Color.ButtonSecondary; [Parameter] public string? IconUrl { get; set; } [Parameter] public char? ShortcutKey { get => shortcutKey; set { if(value is not null && !char.IsLetter((char) value)) throw new System.ArgumentException("Cannot assign a non-letter to a shortcut"); shortcutKey = value is null ? null : char.ToLower((char) value); } } [Parameter] public int? FontSize { get; set; } private Dictionary Attributes => new() { ["data-keyboard-shortcut"] = ShortcutKey, ["disabled"] = isLoading, ["style"] = CssStyle }; private string CssStyle => string.Join("", [ $"--col-pri:{BackgroundColor.CssString};", FontSize is not null ? $"font-size:{FontSize}pt;" : null ]); private ElementReference button; private Timer loadingTimer; private char? shortcutKey; private object triggerLock = new(); private bool isRunning = false; private bool isLoading = false; protected override void OnInitialized() { loadingTimer = new(50) { AutoReset = false }; loadingTimer.Elapsed += TimerElapsed; } // Most button-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 button if the delegate task // takes longer than 50ms private void TimerElapsed(object? sender, EventArgs e) { lock(triggerLock) { if(isRunning) isLoading = true; 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; } } await InvokeAsync(() => StateHasChanged()); if(error) await js.InvokeVoidAsync("triggerButtonError", button); } public void Dispose() => loadingTimer.Dispose(); }