Open pollardpj opened 1 year ago
I think I figured out what I was doing wrong. I needed:
[Parameter]
public Expression<Func<string>> ValueExpression { get; set; }
In this updated custom component, validation messages appear as expected:
@using System.Linq.Expressions
<HxInputText Value="@Value" Label="@Label" ValueChanged="OnValueChanged" ValueExpression="ValueExpression">
<InputGroupEndTemplate>
<HxButton Color="ThemeColor.Primary" Text="Test" OnClick="@(() => OnValueChanged("Test"))" />
</InputGroupEndTemplate>
</HxInputText>
@code {
[Parameter]
public string Label { get; set; }
[Parameter]
public string Value { get; set; }
[Parameter]
public EventCallback<string> ValueChanged { get; set; }
[Parameter]
public Expression<Func<string>> ValueExpression { get; set; }
private async Task OnValueChanged(string value)
{
Value = value;
await ValueChanged.InvokeAsync(value);
}
}
Is this the way I should implement a custom string based component?
If you want to get "all the good stuff" you have to derive from HxInputBase
.
Usually you follow this pattern:
HxInputBase
and delegating the input-part to the internal component created above.E.g.
using System.Diagnostics.CodeAnalysis;
using Havit.Blazor.Components.Web.Bootstrap.Internal;
using Havit.Bt.Blazor.Components.Forms.Internal;
using Havit.Bt.Blazor.Internal;
using Microsoft.AspNetCore.Components.Rendering;
namespace Havit.Bt.Blazor;
public class BtSingleSelect<TValue, TItem> : HxInputBase<TValue>, IInputWithTipLabel, IInputWithSize, IInputWithPlaceholder
{
[Parameter] public BtSingleSelectSettings Settings { get; set; }
protected override BtSingleSelectSettings GetSettings() => this.Settings;
/// <inheritdoc />>
[Parameter] public bool TipVisible { get; set; } = false;
/// <inheritdoc />>
[Parameter] public string TipText { get; set; } = IInputWithTipLabel.DefaultTipText;
/// <inheritdoc />>
[Parameter] public Func<TItem, string> TextSelector { get; set; } = x => x.ToString();
/// <inheritdoc />>
[Parameter] public Func<TItem, TValue> ValueSelector { get; set; }
/// <inheritdoc />>
[Parameter] public IList<TItem> Items { get; set; } = new List<TItem>();
/// <inheritdoc />>
[Parameter] public PopperOffset Offset { get; set; } = new PopperOffset(0, 0);
/// <inheritdoc />>
[Parameter] public Func<TItem, IComparable> SortKeySelector { get; set; }
/// <inheritdoc />>
[Parameter] public bool AutoSort { get; set; } = true;
/// <inheritdoc />>
[Parameter] public string Placeholder { get; set; }
/// <inheritdoc />>
[Parameter] public EventCallback<string> TextChanged { get; set; }
/// <inheritdoc />>
[Parameter] public string EmptyDataText { get; set; }
/// <inheritdoc />>
[Parameter] public int? MenuMaxHeight { get; set; }
protected int MenuMaxHeightEffective => this.MenuMaxHeight ?? this.GetSettings()?.MenuMaxHeight ?? this.GetDefaults().MenuMaxHeight ?? throw new InvalidOperationException(nameof(MenuMaxHeight) + " default for " + nameof(BtSingleSelect) + " has to be set.");
/// <summary>
/// Custom CSS Class to render with dropdown items.
/// </summary>
[Parameter] public string ItemCssClass { get; set; }
/// <summary>
/// Size of the input.
/// </summary>
[Parameter] public InputSize? InputSize { get; set; }
protected InputSize InputSizeEffective => this.InputSize ?? GetSettings()?.InputSize ?? GetDefaults()?.InputSize ?? throw new InvalidOperationException(nameof(InputSize) + " default for " + nameof(HxInputNumber) + " has to be set.");
InputSize IInputWithSize.InputSizeEffective => this.InputSizeEffective;
[Parameter] public string SoftValidationWarning { get; set; }
[Parameter] public bool? Nullable { get; set; }
protected bool NullableEffective
{
get
{
if (Nullable is not null)
{
return Nullable.Value;
}
if (System.Nullable.GetUnderlyingType(typeof(TValue)) is not null)
{
return true;
}
if (typeof(TValue).IsClass)
{
return true;
}
return false;
}
}
[Parameter] public string NullText { get; set; }
protected override BtSingleSelectSettings GetDefaults() => BtSingleSelect.Defaults;
protected override void BuildRenderValidationMessage(RenderTreeBuilder builder)
{
if (!string.IsNullOrEmpty(SoftValidationWarning))
{
builder.OpenRegion(1);
builder.OpenElement(1, "div");
builder.AddAttribute(2, "class", "is-warning");
builder.CloseElement();
builder.OpenElement(10, "div");
builder.AddAttribute(11, "class", "warning-feedback feedback-floating position-absolute top-100 start-0 text-truncate");
builder.AddContent(12, this.SoftValidationWarning);
builder.CloseElement();
builder.CloseRegion();
}
base.BuildRenderValidationMessage(builder);
}
protected override string GetInputCssClassToRender()
{
var cssClass = base.GetInputCssClassToRender();
if (!string.IsNullOrEmpty(SoftValidationWarning))
{
cssClass = CssClassHelper.Combine(cssClass, "is-warning");
}
return cssClass;
}
protected override void BuildRenderInput(RenderTreeBuilder builder)
{
builder.OpenRegion(3000);
builder.OpenComponent<BtSingleSelectInternal<TValue, TItem>>(3001);
builder.AddAttribute(3003, nameof(BtSingleSelectInternal<TValue, TItem>.TextSelector), TextSelector);
builder.AddAttribute(3004, nameof(BtSingleSelectInternal<TValue, TItem>.Items), Items);
builder.AddAttribute(3005, nameof(BtSingleSelectInternal<TValue, TItem>.Offset), Offset);
builder.AddAttribute(3006, nameof(BtSingleSelectInternal<TValue, TItem>.SortKeySelector), SortKeySelector);
builder.AddAttribute(3007, nameof(BtSingleSelectInternal<TValue, TItem>.AutoSort), AutoSort);
builder.AddAttribute(3008, nameof(BtSingleSelectInternal<TValue, TItem>.Placeholder), Placeholder);
builder.AddAttribute(3009, nameof(BtSingleSelectInternal<TValue, TItem>.TextChanged), TextChanged);
builder.AddAttribute(3010, nameof(BtSingleSelectInternal<TValue, TItem>.CssClass), CssClass);
builder.AddAttribute(3011, nameof(BtSingleSelectInternal<TValue, TItem>.EnabledEffective), EnabledEffective);
builder.AddAttribute(3012, nameof(BtSingleSelectInternal<TValue, TItem>.Value), Value);
builder.AddAttribute(3013, nameof(BtSingleSelectInternal<TValue, TItem>.ValueChanged), EventCallback.Factory.Create<TValue>(this, HandleValueChanged));
builder.AddAttribute(3014, nameof(BtSingleSelectInternal<TValue, TItem>.ValueSelector), ValueSelector);
builder.AddAttribute(3015, nameof(BtSingleSelectInternal<TValue, TItem>.EmptyDataText), EmptyDataText);
builder.AddAttribute(3016, nameof(BtSingleSelectInternal<TValue, TItem>.MenuMaxHeightEffective), MenuMaxHeightEffective);
builder.AddAttribute(3017, nameof(BtSingleSelectInternal<TValue, TItem>.InputCssClass), GetInputCssClassToRender());
builder.AddAttribute(3018, nameof(BtSingleSelectInternal<TValue, TItem>.ItemCssClass), ItemCssClass);
builder.AddAttribute(3019, nameof(BtSingleSelectInternal<TValue, TItem>.InputName), Label);
builder.AddAttribute(3020, nameof(BtSingleSelectInternal<TValue, TItem>.NullableEffective), NullableEffective);
builder.AddAttribute(3021, nameof(BtSingleSelectInternal<TValue, TItem>.NullText), NullText);
builder.CloseComponent();
builder.CloseRegion();
}
private void HandleValueChanged(TValue newValue)
{
CurrentValue = newValue; // setter includes ValueChanged + NotifyFieldChanged
}
protected override bool TryParseValueFromString(string value, [MaybeNullWhen(false)] out TValue result, [NotNullWhen(false)] out string validationErrorMessage)
{
throw new NotImplementedException();
}
}
The HxInputBase
is and abstract
class which forces you to implement:
protected abstract InputSettings GetSettings();
- see Defaults & Settingsprotected abstract void BuildRenderInput(RenderTreeBuilder builder);
- where you render the input itself (usually by delegating the rendering to a standalone UI component)...all the other virtual
methods allow you to customize some feature of HxInputBase
.
I'll take this as a suggestion to add some guidelines to the documentation, will track this in our backlog.
Thanks @hakenr , that's clearer to me now, I will give it a go when I feel brave enough :)
I have a requirement to create a custom component, in this case, a date/time picker, which I want to do according to best practice. I am struggling to get validation messages showing within EditForm, I must be doing something fundamentally wrong. I can put this code into a branch for someone to tell me what I should do, but essentially, I want to have some custom code in a component, simple example:
And use it in EditForm:
But validation messages only show for the HxInputText component:
What am I failing to do here?