diff --git a/samples/Sample.ClientSide/Sample.ClientSide.csproj b/samples/Sample.ClientSide/Sample.ClientSide.csproj index 1c08391a..e894da75 100644 --- a/samples/Sample.ClientSide/Sample.ClientSide.csproj +++ b/samples/Sample.ClientSide/Sample.ClientSide.csproj @@ -4,8 +4,6 @@ enable false - - false diff --git a/samples/Sample.Core/Pages/BusyButton/Index.razor b/samples/Sample.Core/Pages/BusyButton/Index.razor index e27c7185..ad56de48 100644 --- a/samples/Sample.Core/Pages/BusyButton/Index.razor +++ b/samples/Sample.Core/Pages/BusyButton/Index.razor @@ -25,6 +25,7 @@
- -
- Saving ... + Saving... Save @@ -50,6 +49,17 @@
+ +
+ + Process + +
+ @code { [Parameter] public bool IsBusy { get; set; } = true; @@ -57,4 +67,8 @@ [Parameter] public bool IsDisabled { get; set; } = false; + public async Task Process() + { + await Task.Delay(2000); + } } diff --git a/src/LoreSoft.Blazor.Controls/BusyButton.cs b/src/LoreSoft.Blazor.Controls/BusyButton.cs index 5c948884..e6ad37a0 100644 --- a/src/LoreSoft.Blazor.Controls/BusyButton.cs +++ b/src/LoreSoft.Blazor.Controls/BusyButton.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; +using Microsoft.AspNetCore.Components.Web; namespace LoreSoft.Blazor.Controls; @@ -11,6 +12,9 @@ public class BusyButton : ComponentBase [Parameter] public bool Disabled { get; set; } + [Parameter] + public string BusyText { get; set; } = "Processing"; + [Parameter] public RenderFragment BusyTemplate { get; set; } @@ -20,30 +24,80 @@ public class BusyButton : ComponentBase [Parameter(CaptureUnmatchedValues = true)] public Dictionary Attributes { get; set; } = new Dictionary(); + [Parameter] + public EventCallback Trigger { get; set; } + + private bool Executing { get; set; } + /// protected override void BuildRenderTree(RenderTreeBuilder builder) { builder.OpenElement(0, "button"); builder.AddMultipleAttributes(1, Attributes); - builder.AddAttribute(2, "disabled", Disabled || Busy); + builder.AddAttribute(2, "disabled", Disabled || IsBusy); + + if (Trigger.HasDelegate) + { + builder.AddAttribute(3, "type", "button"); + builder.AddAttribute(4, "onclick", EventCallback.Factory.Create(this, ExecuteTrigger)); + } - if (Busy) + if (IsBusy) { - builder.AddContent(3, BusyTemplate); + builder.AddContent(5, BusyTemplate); } else { - builder.AddContent(3, ChildContent); + builder.AddContent(6, ChildContent); } builder.CloseElement(); // button } + /// protected override void OnParametersSet() { base.OnParametersSet(); - BusyTemplate ??= builder => builder.AddContent(0, "Busy..."); + BusyTemplate ??= builder => { + builder.AddContent(0, BusyText); + + builder.OpenElement(1, "span"); + builder.AddAttribute(2, "aria-hidden", "true"); + builder.AddAttribute(3, "class", "busy-loading-indicator"); + + builder.OpenElement(4, "span"); + builder.AddAttribute(5, "class", "busy-loading-dot-1"); + builder.CloseElement(); + + builder.OpenElement(6, "span"); + builder.AddAttribute(7, "class", "busy-loading-dot-2"); + builder.CloseElement(); + + builder.OpenElement(8, "span"); + builder.AddAttribute(9, "class", "busy-loading-dot-3"); + builder.CloseElement(); + + builder.CloseElement(); //span + }; + } + + protected bool IsBusy => Busy || Executing; + + private async Task ExecuteTrigger() + { + if (!Trigger.HasDelegate) + return; + + try + { + Executing = true; + await Trigger.InvokeAsync(); + } + finally + { + Executing = false; + } } } diff --git a/src/LoreSoft.Blazor.Controls/StateProvider.razor b/src/LoreSoft.Blazor.Controls/StateProvider.razor new file mode 100644 index 00000000..21f0d494 --- /dev/null +++ b/src/LoreSoft.Blazor.Controls/StateProvider.razor @@ -0,0 +1,5 @@ +@typeparam TState + + + @ChildContent + diff --git a/src/LoreSoft.Blazor.Controls/StateProvider.razor.cs b/src/LoreSoft.Blazor.Controls/StateProvider.razor.cs new file mode 100644 index 00000000..8eae1292 --- /dev/null +++ b/src/LoreSoft.Blazor.Controls/StateProvider.razor.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Components; + +namespace LoreSoft.Blazor.Controls; + +public partial class StateProvider +{ + [Parameter] + public required RenderFragment ChildContent { get; set; } + + public event Action OnStateChange; + + public TState? State { get; protected set; } + + public virtual void Set(TState model) + { + State = model; + NotifyStateChanged(this); + } + + public void NotifyStateChanged(object sender = null) + { + StateHasChanged(); + OnStateChange?.Invoke(sender); + } +} \ No newline at end of file diff --git a/src/LoreSoft.Blazor.Controls/Utilities/ExpandTracker.cs b/src/LoreSoft.Blazor.Controls/Utilities/ExpandTracker.cs new file mode 100644 index 00000000..b61a1dc0 --- /dev/null +++ b/src/LoreSoft.Blazor.Controls/Utilities/ExpandTracker.cs @@ -0,0 +1,34 @@ +namespace LoreSoft.Blazor.Controls.Utilities; + +public class ExpandTracker +{ + private readonly HashSet _expandedItems = []; + + public event Action OnChange; + + public bool IsExpanded(TItem item) + { + return _expandedItems.Contains(item); + } + + public void Toggle(TItem item) + { + if (_expandedItems.Contains(item)) + _expandedItems.Remove(item); + else + _expandedItems.Add(item); + + NotifyStateChanged(); + } + + public virtual void Clear() + { + _expandedItems.Clear(); + NotifyStateChanged(); + } + + public void NotifyStateChanged() + { + OnChange?.Invoke(); + } +} \ No newline at end of file diff --git a/src/LoreSoft.Blazor.Controls/wwwroot/BlazorControls.css b/src/LoreSoft.Blazor.Controls/wwwroot/BlazorControls.css index 746d3383..58ef0a02 100644 --- a/src/LoreSoft.Blazor.Controls/wwwroot/BlazorControls.css +++ b/src/LoreSoft.Blazor.Controls/wwwroot/BlazorControls.css @@ -1022,4 +1022,48 @@ button.data-grid-header { .loading-block-text { color: #212529; +} + +/* BusyButton */ + +.busy-loading-indicator { + align-self: center; + background-color: transparent; + border: 0; + box-sizing: border-box; + color: #ccc; + display: inline-block; + font-size: 4px; + line-height: 1; + margin-right: 4px; + outline: none; + padding: 8px; + text-align: center; + transition: color 150ms ease 0s; + vertical-align: middle +} + +.busy-loading-dot-1, .busy-loading-dot-2, .busy-loading-dot-3 { + background-color: #ccc; + border-radius: 1em; + display: inline-block; + height: 1em; + vertical-align: top; + width: 1em +} + +.busy-loading-dot-2, .busy-loading-dot-3 { + margin-left: 1em +} + +.busy-loading-dot-1 { + animation: 1s ease-in-out 0ms infinite normal none running dot-loading +} + +.busy-loading-dot-2 { + animation: 1s ease-in-out 160ms infinite normal none running dot-loading +} + +.busy-loading-dot-3 { + animation: 1s ease-in-out 320ms infinite normal none running dot-loading } \ No newline at end of file