Blazor Wrap InputCheckbox in Custom Element

161 Views Asked by At

I am trying to create a custom checkbox for use in an EditForm using .NET 8. My checkbox initially was implemented using a button which just toggles the value. I was unable to get this to work in an EditForm though because I cannot make the EditForm render using "InteractiveServer" as I need the HttpContext.

My current approach is to simply make my checkbox inherit from InputCheckbox and then use an InputCheckbox element inside it which handles the functionality. Unfortunately, I cannot get this to work. I have already implemented a similar approach for a custom TextInput. The code for this looks like this:

@inherits InputText

@using ProjectName.Data.Utils;

<FormElementBase IconClass="@IconClass" LabelText="@LabelText">
    <InputText name=@NameAttributeValue type=@(PasswordInput ? "password" : "text") placeholder="Input..." class="text-input" @bind-Value=CurrentValue />
</FormElementBase>

@code {
    [Parameter, EditorRequired]
    public string IconClass { get; set; } = IconClasses.Default;

    [Parameter, EditorRequired]
    public string LabelText { get; set; } = "Label";

    [Parameter]
    public bool PasswordInput { get; set; } = false;
}

My current checkbox implementation looks quite similar:

@inherits InputCheckbox

<label for="chk">
    <InputCheckbox id="chk" @bind-Value=CurrentValue />
    Test
</label>

However, the input does not actually change the model value in the EditForm for the checkbox. The text input works as expected. They are used in the EditForm like this:

<EditForm Model="Input" method="post" FormName=@FORM_NAME class="register-form" OnSubmit="Test">
  ...
  <TextInput
    [email protected]
    LabelText="E-Mail"
    @bind-Value=Input.Email
  />
  <CheckBox @bind-Value=Input.AgreedToTos />
  ...
</EditForm>

I have also tried using a checkbox that does not inherit from InputBase and making it render using rendermode InteractiveServer. The ValueChanged handlers are then called but the actual value of the model is not updated. When using rendermode InteractiveServer with the checkbox inheriting from InputBase or InputCheckbox, I get the following exception: NotSupportedException: Serialization and deserialization of 'System.Type' instances is not supported.

I have also tried putting the InputCheckbox inside of the EditForm directly, which does work as expected.

My question now is, how do I get my custom checkbox to work and why does InputText work when wrapping it inside of another element but InputCheckbox does not?

An alternative solution would be to just implement the custom checkbox directly into the EditForm. But I want to use this approach only if it is not possible otherwise, as I would like to be able to reuse my custom checkbox in other places.

1

There are 1 best solutions below

4
On

You are on the right track in inheriting from the InputBase classes. There are several ways to do this.

Here are two button implementations to consider.

They demonstrate how to implement basic input context implementations with binding.

CheckButton

@using System.Diagnostics.CodeAnalysis
@inherits InputBase<bool>

<div>
    <button class="@_buttonCss" @onclick="this.Switch">@_buttonText</button>
</div>

@code {
    private string _buttonText => this.Value ? "On" : "Off";
    private string _buttonCss => this.Value ? "btn btn-success" : "btn btn-danger";

    protected override bool TryParseValueFromString(string? value, out bool result, [NotNullWhen(false)] out string? validationErrorMessage)
        => throw new NotSupportedException($"This component does not parse string inputs. Bind to the '{nameof(CurrentValue)}' property, not '{nameof(CurrentValueAsString)}'.");

    private void Switch()
        => this.CurrentValue = !Value;
}

SelectButton

@using System.Diagnostics.CodeAnalysis
@inherits InputBase<SelectButtonStatus>


<div class="btn-group m-2" role="group" aria-label="Basic example">
    <button type="button" class="@this.GetCss(SelectButtonStatus.First)" @onclick="() => SetValue(SelectButtonStatus.First)">First</button>
    <button type="button" class="@this.GetCss(SelectButtonStatus.Second)" @onclick="() => SetValue(SelectButtonStatus.Second)">Second</button>
    <button type="button" class="@this.GetCss(SelectButtonStatus.Third)" @onclick="() => SetValue(SelectButtonStatus.Third)">Third</button>
</div>

@code {
    private string GetCss(SelectButtonStatus status)
        => status == this.Value ? "btn btn-primary" : "btn btn-outline-primary";

    protected override bool TryParseValueFromString(string? value, out SelectButtonStatus result, [NotNullWhen(false)] out string? validationErrorMessage)
        => throw new NotSupportedException($"This component does not parse string inputs. Bind to the '{nameof(CurrentValue)}' property, not '{nameof(CurrentValueAsString)}'.");

    private void SetValue(SelectButtonStatus status)
        => this.CurrentValue = status;
}

And:


public enum SelectButtonStatus
{
    First = 1,
    Second = 2,
    Third = 3
}

Demo

And a demno page:

@page "/"

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<CheckButton @bind-Value="_on" />

<SelectButton @bind-Value="_status" />

<div class="bg-dark text-white p-2 m-2">
    <pre>Value: @(_on ? "On" : "Off")</pre>
    <pre>Status: @(Enum.GetName(typeof(SelectButtonStatus), _status))</pre>
</div>

@code {
    private bool _on;
    private SelectButtonStatus _status = SelectButtonStatus.First;
}

enter image description here