feat(Checkbox): add OnBeforeStateChanged parameter (#4005)

* feat: 增加 OnBeforeStateChanged 回调方法

* refactor: 增加客户端阻止逻辑

* refactor: 增加阻止继承标签

* feat: 支持 OnBeforeStateChanged 回调方法

* doc: 格式化代码

* doc: 增加示例文档

* refactor: 重构代码

* test: 更新单元测试
This commit is contained in:
Argo Zhang 2024-08-09 14:46:03 +08:00 committed by GitHub
parent e9e23e66fe
commit fbfa97c528
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 162 additions and 29 deletions

View File

@ -99,7 +99,6 @@
<Checkbox TValue="string" State="CheckboxState.Checked" ShowAfterLabel="true" DisplayText="ExtraExtraLarge" Size="Size.ExtraExtraLarge" />
</div>
</div>
</DemoBlock>
<DemoBlock Title="@Localizer["OnStateChangedTitle"]" Introduction="@Localizer["OnStateChangedIntro"]" Name="OnStateChanged">
@ -114,6 +113,17 @@
<ConsoleLogger @ref="OnStateChangedLogger" />
</DemoBlock>
<DemoBlock Title="@Localizer["OnBeforeStateChangedTitle"]" Introduction="@Localizer["OnBeforeStateChangedIntro"]" Name="OnBeforeStateChanged">
<div class="row g-3 form-inline">
<div class="col-12 col-sm-6">
<Checkbox TValue="bool" DisplayText="@Localizer["OnBeforeStateChangedText"]" ShowLabel="true" @bind-Value="SelectedValue" OnBeforeStateChanged="@OnBeforeStateChanged" />
</div>
<div class="col-12 col-sm-6">
<Display @bind-Value="@SelectedValue" />
</div>
</div>
</DemoBlock>
<DemoBlock Title="@Localizer["BindStringTitle"]" Introduction="@Localizer["BindStringIntro"]" Name="BindString">
<div class="row g-3 form-inline">
<div class="col-12">

View File

@ -11,6 +11,9 @@ namespace BootstrapBlazor.Server.Components.Samples;
/// </summary>
public sealed partial class Checkboxs
{
[Inject, NotNull]
private SwalService? SwalService { get; set; }
private Foo Model { get; set; } = new Foo();
private class Foo
@ -53,6 +56,14 @@ public sealed partial class Checkboxs
return Task.CompletedTask;
}
private bool SelectedValue { get; set; }
private Task<bool> OnBeforeStateChanged(CheckboxState state) => SwalService.ShowModal(new SwalOption()
{
Title = Localizer["OnBeforeStateChangedSwalTitle"],
Content = Localizer["OnBeforeStateChangedSwalContent"]
});
/// <summary>
/// GetAttributes
/// </summary>
@ -107,16 +118,22 @@ public sealed partial class Checkboxs
/// <returns></returns>
private EventItem[] GetEvents() =>
[
new()
{
Name = "OnBeforeStateChanged",
Description = Localizer["OnBeforeStateChanged"],
Type ="Action<CheckboxState, TItem>"
},
new()
{
Name = "OnStateChanged",
Description = Localizer["Event1"],
Description = Localizer["OnStateChanged"],
Type ="Action<CheckboxState, TItem>"
},
new()
{
Name = "StateChanged",
Description = Localizer["Event2"],
Description = Localizer["StateChanged"],
Type ="EventCallback<CheckboxState>"
}
];

View File

@ -2366,13 +2366,19 @@
"ValidateFormTitle": "Used in forms",
"ValidateFormIntro": "When you use <code>Checkbox</code> in a form, the display label text is placed in front of the component",
"ValidateFormDescription": "The pre-label explicit rules are consistent with the <code>BootstrapInput</code> component <a href='input'>[portal]</a>",
"OnBeforeStateChangedTitle": "OnBeforeStateChanged",
"OnBeforeStateChangedIntro": "By setting the <code>OnBeforeStateChanged</code> callback method, you can cancel the state change logic",
"OnBeforeStateChangedText": "Confirm",
"OnBeforeStateChangedSwalTitle": "Confirm Select",
"OnBeforeStateChangedSwalContent": "Whether the current checkbox is selected",
"Att1": "Whether to display the front label",
"Att2": "Whether to display the rear label",
"Att3": "The front label displays text",
"Att4": "Whether to disable it",
"Att5": "The type of control",
"Event1": "This method is called back when the selection box state changes",
"Event2": "The state changes the callback method",
"OnBeforeStateChanged": "This method is called back before the selection box status changes",
"OnStateChanged": "This method is called back when the selection box state changes",
"StateChanged": "The state changes the callback method",
"StatusText1": "Selected",
"StatusText2": "Not selected",
"StatusText3": "Indeterminate",

View File

@ -2366,13 +2366,19 @@
"ValidateFormTitle": "表单中使用",
"ValidateFormIntro": "在表单中使用 <code>Checkbox</code> 时,显示标签文字会放置到组件前面",
"ValidateFormDescription": "前置标签显式规则与 <code>BootstrapInput</code> 组件一致 <a href='input'>[传送门]</a>",
"OnBeforeStateChangedTitle": "选中前回调方法",
"OnBeforeStateChangedIntro": "通过设置 <code>OnBeforeStateChanged</code> 回调方法,可取消选中逻辑",
"OnBeforeStateChangedText": "弹窗确认",
"OnBeforeStateChangedSwalTitle": "弹窗确认",
"OnBeforeStateChangedSwalContent": "是否更改选中状态",
"Att1": "是否显示前置标签",
"Att2": "是否显示后置标签",
"Att3": "前置标签显示文本",
"Att4": "是否禁用",
"Att5": "控件类型",
"Event1": "选择框状态改变时回调此方法",
"Event2": "State 状态改变回调方法",
"OnBeforeStateChanged": "选择框状态改变前回调此方法",
"OnStateChanged": "选择框状态改变时回调此方法",
"StateChanged": "State 状态改变回调方法",
"StatusText1": "选中",
"StatusText2": "未选",
"StatusText3": "不确定",

View File

@ -20,7 +20,7 @@ else
@code {
RenderFragment RenderCheckbox =>
@<div @attributes="AdditionalAttributes" class="@ClassString">
<DynamicElement TagName="input" class="@InputClassString" type="checkbox" id="@Id" disabled="@Disabled" checked="@CheckedString" TriggerClick="!IsDisabled" OnClick="OnToggleClick" StopPropagation="StopPropagation" />
<DynamicElement TagName="input" class="@InputClassString" type="checkbox" id="@Id" disabled="@Disabled" checked="@CheckedString" TriggerClick="TriggerClick" OnClick="OnToggleClick" StopPropagation="StopPropagation" />
@if (IsShowAfterLabel)
{
@RenderLabel

View File

@ -9,7 +9,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// Checkbox 组件
/// </summary>
[BootstrapModuleAutoLoader(ModuleName = "utility", AutoInvokeInit = false, AutoInvokeDispose = false)]
[BootstrapModuleAutoLoader(JSObjectReference = true)]
public partial class Checkbox<TValue> : ValidateBase<TValue>
{
/// <summary>
@ -81,6 +81,12 @@ public partial class Checkbox<TValue> : ValidateBase<TValue>
[Parameter]
public EventCallback<CheckboxState> StateChanged { get; set; }
/// <summary>
/// 获得/设置 选中状态改变前回调此方法 返回 false 可以阻止状态改变
/// </summary>
[Parameter]
public Func<CheckboxState, Task<bool>>? OnBeforeStateChanged { get; set; }
/// <summary>
/// 获得/设置 选择框状态改变时回调此方法
/// </summary>
@ -143,6 +149,41 @@ public partial class Checkbox<TValue> : ValidateBase<TValue>
await InvokeVoidAsync("setIndeterminate", Id, State == CheckboxState.Indeterminate);
}
/// <summary>
/// <inheritdoc/>
/// </summary>
/// <returns></returns>
protected override async Task InvokeInitAsync()
{
if (OnBeforeStateChanged != null)
{
await InvokeVoidAsync("init", Id, Interop, new { Callback = nameof(TriggerOnBeforeStateChanged) });
}
}
/// <summary>
/// 触发 OnBeforeStateChanged 回调方法 由 JavaScript 调用
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
[JSInvokable]
public async Task TriggerOnBeforeStateChanged()
{
if (OnBeforeStateChanged != null)
{
var state = State == CheckboxState.Checked ? CheckboxState.UnChecked : CheckboxState.Checked;
var ret = await OnBeforeStateChanged(state);
if (ret)
{
var render = await InternalStateChanged(state);
if (render)
{
StateHasChanged();
}
}
}
}
/// <summary>
/// 点击选择框方法
/// </summary>
@ -150,39 +191,44 @@ public partial class Checkbox<TValue> : ValidateBase<TValue>
{
if (!IsDisabled)
{
_paddingStateChanged = true;
await InternalStateChanged(State == CheckboxState.Checked ? CheckboxState.UnChecked : CheckboxState.Checked);
}
}
private bool TriggerClick => !IsDisabled && OnBeforeStateChanged == null;
/// <summary>
/// 此变量为了提高性能,避免循环更新
/// </summary>
private bool _paddingStateChanged;
private async Task InternalStateChanged(CheckboxState state)
private async Task<bool> InternalStateChanged(CheckboxState state)
{
if (_paddingStateChanged)
var ret = true;
_paddingStateChanged = true;
if (IsBoolean)
{
if (IsBoolean)
CurrentValue = (TValue)(object)(state == CheckboxState.Checked);
}
if (State != state)
{
State = state;
if (StateChanged.HasDelegate)
{
CurrentValue = (TValue)(object)(state == CheckboxState.Checked);
await StateChanged.InvokeAsync(State);
ret = false;
}
if (State != state)
if (OnStateChanged != null)
{
State = state;
if (StateChanged.HasDelegate)
{
await StateChanged.InvokeAsync(State);
}
if (OnStateChanged != null)
{
await OnStateChanged.Invoke(State, Value);
}
await OnStateChanged(State, Value);
}
}
return ret;
}
/// <summary>
@ -193,10 +239,11 @@ public partial class Checkbox<TValue> : ValidateBase<TValue>
{
if (!_paddingStateChanged)
{
_paddingStateChanged = true;
await InternalStateChanged(state);
StateHasChanged();
var render = await InternalStateChanged(state);
if (render)
{
StateHasChanged();
}
}
}

View File

@ -0,0 +1,25 @@
import { setIndeterminate } from "../../modules/utility.js"
import EventHandler from "../../modules/event-handler.js"
export function init(id, invoke, options) {
const el = document.getElementById(id);
if (el === null) {
return;
}
EventHandler.on(el, 'click', async e => {
e.preventDefault();
await invoke.invokeMethodAsync(options.callback);
})
}
export function dispose(id) {
const el = document.getElementById(id);
if (el === null) {
return;
}
EventHandler.off(el, 'click');
}
export { setIndeterminate }

View File

@ -7,6 +7,7 @@ namespace BootstrapBlazor.Components;
/// <summary>
/// Radio 单选框组件
/// </summary>
[JSModuleNotInherited]
public partial class Radio<TValue> : Checkbox<TValue>
{
/// <summary>

View File

@ -47,6 +47,27 @@ public class CheckboxListTest : BootstrapBlazorTestBase
span.MarkupMatches("<span tabindex=\"0\" diff:ignore data-bs-original-title=\"Test\" data-bs-toggle=\"tooltip\" data-bs-placement=\"top\" data-bs-trigger=\"focus hover\">Test</span>");
}
[Fact]
public async Task Checkbox_OnBeforeStateChanged()
{
var confirm = true;
var cut = Context.RenderComponent<Checkbox<bool>>(builder =>
{
builder.Add(a => a.OnBeforeStateChanged, state =>
{
return Task.FromResult(confirm);
});
});
Assert.False(cut.Instance.Value);
await cut.InvokeAsync(cut.Instance.TriggerOnBeforeStateChanged);
Assert.True(cut.Instance.Value);
confirm = false;
await cut.InvokeAsync(cut.Instance.TriggerOnBeforeStateChanged);
Assert.True(cut.Instance.Value);
}
[Fact]
public void Checkbox_Dispose()
{