Need a little help creating custom select component
I am attempting to create a custom form select component. The component will contain my own custom markup rather than using the tag as it needs a completely different UI beyond which I can style with css.
The component should be able to bind it's value to a string / int / decimal model property which is where I am having trouble.
This is what I have so far:
MySelect.razor
@typeparam TValue
@inherits InputBase<TValue>
@namespace Accounting.Web.Components
@foreach (var option in Options)
{
<button @onclick="OnClick(option.Value)">@option.Value</button>
}
MySelect.razor.cs
namespace Accounting.Web.Components
{
public partial class MySelectOption<TValue>
{
public int Id { get; set; }
public TValue Value { get; set; }
}
public partial class MySelect<TValue> : InputBase<TValue>
{
[Parameter]
public string Id { get; set; } = "ESelect";
[Parameter]
public List<MySelectOption<TValue>> Options { get; set; }
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
throw new NotImplementedException();
}
public void OnClick(TValue value)
{
Value = value;
}
}
}
And then in Index.razor:
<MySelect Options="@options" @bind-Value="AddDto.Description" TValue="string">
So when using the component I should be able to bind it to a property of any type (but usually int or string) which I pass as the type param TValue.
However, the line below is causing an issue:
<button @onclick="OnClick(option.Value)">@option.Value</button>
Argument 2: cannot convert from 'void' to 'Microsoft.AspNetCore.Components.EventCallback'
How can I pass the option.Value (which is always a string) to the onCLick event? Or alternatively modify the code above so that I can accomplish my initially stated goal?
CodePudding user response:
You have more that one issue, but the important one is trying to update Value
. Value is an "input" into the control. The updated value is passed back to parent by calling ValueChanged
. However, calling ValueChanged
directly bypasses the built in functionality in InputBase
and it's interaction with the EditContext
.
This demonstrates the basics of inheriting from InputBase
.
To leverage the built in functionality, you need to either:
- Set the value by setting
CurrentValueAsString
from the markup and then providing a customTryParseValueFromString
to convert from a string to your type (there's aBindConverter
helper you can use - it's whatInputNumber
and other input controls use). - Set the value directly by setting
CurrentValue
. This bypassesTryParseValueFromString
.
Your MySelect
.
I've prettied up your buttons and abstracted your list to an IEnumerable
.
@typeparam TValue
@inherits InputBase<TValue>
@using Microsoft.AspNetCore.Components.Forms;
@using Microsoft.AspNetCore.Components;
@using System.Diagnostics.CodeAnalysis;
<div role="group">
@foreach (var option in Options)
{
<button @onclick="() => OnClick(option.Value)">@option.Value</button>
}
</div>
@code {
[Parameter] public IEnumerable<MySelectOption<TValue>> Options { get; set; } = new List<MySelectOption<TValue>>();
private string btnColour(TValue? value)
{
if (this.Value is null)
return "btn btn-outline-primary";
return this.Value.Equals(value)
? "btn btn-primary"
: "btn btn-outline-primary";
}
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
throw new NotImplementedException();
}
public void OnClick(TValue? value)
{
CurrentValue = value;
}
}
And then here's a demo page to show it in use.
@page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
<EditForm Model=@model>
<MySelect Options="@options" @bind-Value=model.Description TValue="string" />
</EditForm>
<div >
Description: @model.Description
</div>
@code {
private Model model = new();
IEnumerable<MySelectOption<string>> options =
new List<MySelectOption<string>>() {
new MySelectOption<string> { Id = 1, Value = "France" },
new MySelectOption<string> { Id = 1, Value = "Spain" },
new MySelectOption<string> { Id = 1, Value = "Portugal" },
};
public class Model
{
public string? Description { get; set; }
}
}
For reference you can find the source code for all the standard InputBase
controls here:
CodePudding user response:
With help and suggestions from previous answers, below is the solution I arrived at:
Index.razor
<MySelect Options="@options" @bind-Value="AddDto.InvestmentEntityId">
</MySelect>
@AddDto.InvestmentEntityId // integer property
<MySelect Options="@options" @bind-Value="AddDto.Description">
</MySelect>
@AddDto.Description // string property
MySelect.razor
@typeparam TValue
@inherits InputBase<TValue>
@namespace Accounting.Web.Components
@foreach (var option in Options)
{
<button @onclick="() => OnClick(option.Value)">@option.Value</button>
}
MySelect.razor.cs
namespace Accounting.Web.Components
{
public partial class MySelectOption
{
public int Id { get; set; }
public string Value { get; set; }
}
public partial class MySelect<TValue> : InputBase<TValue>
{
[Parameter]
public List<MySelectOption> Options { get; set; }
protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string? validationErrorMessage)
{
if (BindConverter.TryConvertTo<TValue>(value, null, out result))
{
validationErrorMessage = null;
}
else
{
validationErrorMessage = "Err : Select value";
}
}
public void OnClick(string value)
{
TValue tmpValue;
BindConverter.TryConvertTo<TValue>(value, null, out tmpValue);
CurrentValue = tmpValue;
}
}
}
It's probably not perfect but I hope it helps anyone looking to do the same.