0

I have an ExtendedInputText component which inherits from InputText

@inherits InputText

<div class="flex">
    <label class="w-1/2">
        @Label 
        @if(Required){
            <span class="text-red-500 ml-1">*</span>
        }
    </label>
    <InputText
        class="flex-1 border border-gray-200 bg-white p-2 rounded"
        placeholder="@Label"
        Value="@Value"
        ValueChanged="@ValueChanged"
        ValueExpression="@ValueExpression"
        Required="@Required"
    />
    
</div>

@code
{

    [Parameter]
    public bool Required { get; set; }

    [Parameter]
    public string Label { get; set; }
}

I intend on using it to replace this

<EditForm Model="Command" OnValidSubmit="OnValidSubmit">

  <FluentValidationValidator />
  <ValidationSummary />

  <div class="">
    <label>Title <span class="text-red-500">*</span></label>
    <InputText id="Title" @bind-Value="Command.Title" />
    <ValidationMessage For="@(() => Command.Title)" />
  </div>

  <button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>

</EditForm>

with this

<EditForm Model="Command" OnValidSubmit="OnValidSubmit">

  <FluentValidationValidator />
  <ValidationSummary />

  <ExtendedInputText Label="Title" Required="true" @bind-Value="Command.Title"/>

  <button type="submit" class="p-2 bg-positive-500 text-white rounded">Create</button>

</EditForm>

How would I go about also passing <ValidationMessage For="@(() => Command.Title)" /> to the ExtendedInputText component and rendering it from within?

2
  • I have a "composite" component I use that is similar to what you are looking at. I'll post the component code if you wish as an answer and you can pick and choose what you use from it. It builds out a BootStrap flavoured input with optional label and validation information. you can see the control here - cec-blazor-database.azurewebsites.net/fetchdata. Edit a record and clear the temperature. Commented Aug 18, 2021 at 20:36
  • Hi @MrCakaShaunCurtis if your component lets me pass relevant validationmessage content to a component I very much would like to see what you've got thank you! Commented Aug 18, 2021 at 20:43

3 Answers 3

1

With the help of Nicola and Shaun, this is the solution that worked for me.

@inherits InputText

<div class="flex">
    <label class="w-1/2 text-right font-semibold mr-1 py-2">
        @Label
        @if (Required)
        {
            <span class="text-red-500 ml-1">*</span>
        }
    </label>
    <div class="flex-1">
        <InputText class="w-full border border-gray-200 bg-white p-2 rounded"
                    placeholder="@Label"
                    Value="@Value"
                    ValueChanged="@ValueChanged"
                    ValueExpression="@ValueExpression"
                    Required="@Required"/>
        @ValidationFragment
    </div>
</div>

@code
{

    [Parameter]
    public bool Required { get; set; }

    [Parameter]
    public string Label { get; set; }

    private RenderFragment ValidationFragment => (builder) =>
    {
        var messages = EditContext.GetValidationMessages(FieldIdentifier).ToList();
        if(messages is not null && messages.Count > 0)
        {
            builder.OpenElement(310, "div");
            builder.AddAttribute(320, "class", "text-red-500 p-2 w-full");
            builder.OpenComponent<ValidationMessage<string>>(330);
            builder.AddAttribute(340, "For", ValueExpression);
            builder.CloseComponent();
            builder.CloseElement();
        }

    };

}

They key part was the private RenderFragment ValidationFragment which is built programatically to display associated errors stored in the cascading EditContext

Sign up to request clarification or add additional context in comments.

Comments

1

I use the following code for a component I've created LabelText but should be used for your case:

public partial class LabelText<T>: ComponentBase
{
    [Parameter] public Expression<Func<T>> For { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }

    private FieldIdentifier _fieldIdentifier;

    // ...
    protected override void BuildRenderTree(RenderTreeBuilder builder)
    {
        builder.OpenElement(0, "label");
        builder.AddMultipleAttributes(1, AdditionalAttributes);
        builder.AddAttribute(2, "for", _fieldIdentifier.FieldName);
        builder.AddContent(3, label + GetRequired());
        builder.CloseElement();
    }

    protected override void OnParametersSet()
    {
        if (CurrentEditContext == null)
        {
            throw new InvalidOperationException($"{GetType()} requires a cascading parameter " +
                $"of type {nameof(EditContext)}. For example, you can use {GetType()} inside " +
                $"an {nameof(EditForm)}.");
        }

        if (For == null) // Not possible except if you manually specify T
        {
            throw new InvalidOperationException($"{GetType()} requires a value for the " +
                $"{nameof(For)} parameter.");
        }
        _fieldIdentifier = FieldIdentifier.Create(For);
    }
}

UPDATE

I can't explain better than the excellent piece of code from @MrC

private RenderFragment ValidationFragment => (builder) =>
{
    if (this.ShowValidation && !this.IsValid)
    {
        builder.OpenElement(310, "div");
        builder.AddAttribute(320, "class", MessageCss);
        builder.OpenComponent<ValidationMessage<TValue>>(330);
        builder.AddAttribute(340, "For", this.ValueExpression);
        builder.CloseComponent();
        builder.CloseElement();
    }
    else if (!string.IsNullOrWhiteSpace(this.HelperText))
    {
        builder.OpenElement(350, "div");
        builder.AddAttribute(360, "class", MessageCss);
        builder.AddContent(370, this.HelperText);
        builder.CloseElement();
    }
};

You only need to add a parameter like

ValidationMessage="(() => Command.Title)"

and this RenderFragment does the job for you.

4 Comments

Hi thanks for the suggested answer, sorry I don't quite follow how to use this to pass a validationmessage to a component
Thanks again for your input it really helped point me in the right direction. There was actually quite a bit of overlap with what EditForm/InputText offers so I didn't actually have to add much code and the part I was looking for was the private RenderFragment, its a new concept to me!
Please mark one of the answers as correct or useful in case you resolved your issue. Thanks
Thanks again, I will mark my own answer as the accepted one once I can. Ta
0

The full code for a similar component. Note that the component gets the Validation message, you don't need to pass it.

I think I've included the only dependant class, but I may have missed something.

/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================

using Blazr.SPA.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Rendering;
using System;
using System.Linq;
using System.Linq.Expressions;

#nullable enable
#pragma warning disable CS8622 // Nullability of reference types in type of parameter doesn't match the target delegate (possibly because of nullability attributes).
#pragma warning disable CS8602 // Dereference of a possibly null reference.
namespace Blazr.UIComponents
{
    public class FormEditControl<TValue> : ComponentBase
    {
        [Parameter]
        public TValue? Value { get; set; }

        [Parameter] public EventCallback<TValue> ValueChanged { get; set; }

        [Parameter] public Expression<Func<TValue>>? ValueExpression { get; set; }

        [Parameter] public string? Label { get; set; }

        [Parameter] public string? HelperText { get; set; }

        [Parameter] public string DivCssClass { get; set; } = "mb-2";

        [Parameter] public string LabelCssClass { get; set; } = "form-label";

        [Parameter] public string ControlCssClass { get; set; } = "form-control";

        [Parameter] public Type ControlType { get; set; } = typeof(InputText);

        [Parameter] public bool ShowValidation { get; set; }

        [Parameter] public bool ShowLabel { get; set; } = true;

        [Parameter] public bool IsRequired { get; set; }

        [Parameter] public bool IsRow { get; set; }

        [CascadingParameter] EditContext CurrentEditContext { get; set; } = default!;

        private readonly string formId = Guid.NewGuid().ToString();

        private bool IsLabel => this.ShowLabel && (!string.IsNullOrWhiteSpace(this.Label) || !string.IsNullOrWhiteSpace(this.FieldName));

        private bool IsValid;

        private FieldIdentifier _fieldIdentifier;

        private ValidationMessageStore? _messageStore;

        private string? DisplayLabel => this.Label ?? this.FieldName;
        private string? FieldName
        {
            get
            {
                string? fieldName = null;
                if (this.ValueExpression != null)
                    ParseAccessor(this.ValueExpression, out var model, out fieldName);
                return fieldName;
            }
        }

        private string MessageCss => CSSBuilder.Class()
            .AddClass("invalid-feedback", !this.IsValid)
            .AddClass("valid-feedback", this.IsValid)
            .Build();

        private string ControlCss => CSSBuilder.Class(this.ControlCssClass)
            .AddClass("is-valid", this.IsValid)
            .AddClass("is-invalid", !this.IsValid)
            .Build();

        protected override void OnInitialized()
        {
            if (CurrentEditContext is null)
                throw new InvalidOperationException($"No Cascading Edit Context Found!");

            if (ValueExpression is null)
                throw new InvalidOperationException($"No ValueExpression defined for the Control!  Define a Bind-Value.");

            if (!ValueChanged.HasDelegate)
                throw new InvalidOperationException($"No ValueChanged defined for the Control! Define a Bind-Value.");

            CurrentEditContext.OnFieldChanged += FieldChanged;
            CurrentEditContext.OnValidationStateChanged += ValidationStateChanged;
            _messageStore = new ValidationMessageStore(this.CurrentEditContext);
            _fieldIdentifier = FieldIdentifier.Create(ValueExpression);
            if (_messageStore is null)
                throw new InvalidOperationException($"Cannot set the Validation Message Store!");

            var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
            var showHelpText = (messages.Count == 0) && this.IsRequired && this.Value is null;
            if (showHelpText && !string.IsNullOrWhiteSpace(this.HelperText))
                _messageStore.Add(_fieldIdentifier, this.HelperText);
        }

        protected void ValidationStateChanged(object sender, ValidationStateChangedEventArgs e)
        {
            var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
            if (messages != null || messages.Count > 1)
            {
                _messageStore.Clear();
            }
        }

        protected void FieldChanged(object sender, FieldChangedEventArgs e)
        {
            if (e.FieldIdentifier.Equals(_fieldIdentifier))
                _messageStore.Clear();
        }

        protected override void OnParametersSet()
        {
            this.IsValid = true;
            {
                if (this.IsRequired)
                {
                    this.IsValid = false;
                    var messages = CurrentEditContext.GetValidationMessages(_fieldIdentifier).ToList();
                    if (messages is null || messages.Count == 0)
                        this.IsValid = true;
                }
            }
        }

        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            if (IsRow)
                builder.AddContent(1, RowFragment);
            else
                builder.AddContent(2, BaseFragment);
        }

        private RenderFragment BaseFragment => (builder) =>
        {
            builder.OpenElement(0, "div");
            builder.AddAttribute(10, "class", this.DivCssClass);
            builder.AddContent(40, this.LabelFragment);
            builder.AddContent(60, this.ControlFragment);
            builder.AddContent(70, this.ValidationFragment);
            builder.CloseElement();
        };

        private RenderFragment RowFragment => (builder) =>
        {
            builder.OpenElement(0, "div");
            builder.AddAttribute(10, "class", "row form-group");
            builder.OpenElement(20, "div");
            builder.AddAttribute(30, "class", "col-12 col-md-3");
            builder.AddContent(40, this.LabelFragment);
            builder.CloseElement();
            builder.OpenElement(40, "div");
            builder.AddAttribute(50, "class", "col-12 col-md-9");
            builder.AddContent(60, this.ControlFragment);
            builder.AddContent(70, this.ValidationFragment);
            builder.CloseElement();
            builder.CloseElement();
        };

        private RenderFragment LabelFragment => (builder) =>
        {
            if (this.IsLabel)
            {
                builder.OpenElement(110, "label");
                builder.AddAttribute(120, "for", this.formId);
                builder.AddAttribute(130, "class", this.LabelCssClass);
                builder.AddContent(140, this.DisplayLabel);
                builder.CloseElement();
            }
        };


        private RenderFragment ControlFragment => (builder) =>
        {
            builder.OpenComponent(210, this.ControlType);
            builder.AddAttribute(220, "class", this.ControlCss);
            builder.AddAttribute(230, "Value", this.Value);
            builder.AddAttribute(240, "ValueChanged", EventCallback.Factory.Create(this, this.ValueChanged));
            builder.AddAttribute(250, "ValueExpression", this.ValueExpression);
            builder.CloseComponent();
        };

        private RenderFragment ValidationFragment => (builder) =>
        {
            if (this.ShowValidation && !this.IsValid)
            {
                builder.OpenElement(310, "div");
                builder.AddAttribute(320, "class", MessageCss);
                builder.OpenComponent<ValidationMessage<TValue>>(330);
                builder.AddAttribute(340, "For", this.ValueExpression);
                builder.CloseComponent();
                builder.CloseElement();
            }
            else if (!string.IsNullOrWhiteSpace(this.HelperText))
            {
                builder.OpenElement(350, "div");
                builder.AddAttribute(360, "class", MessageCss);
                builder.AddContent(370, this.HelperText);
                builder.CloseElement();
            }
        };

        // Code lifted from FieldIdentifier.cs
        private static void ParseAccessor<T>(Expression<Func<T>> accessor, out object model, out string fieldName)
        {
            var accessorBody = accessor.Body;
            if (accessorBody is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Convert && unaryExpression.Type == typeof(object))
                accessorBody = unaryExpression.Operand;

            if (!(accessorBody is MemberExpression memberExpression))
                throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");

            fieldName = memberExpression.Member.Name;
            if (memberExpression.Expression is ConstantExpression constantExpression)
            {
                if (constantExpression.Value is null)
                    throw new ArgumentException("The provided expression must evaluate to a non-null value.");
                model = constantExpression.Value;
            }
            else if (memberExpression.Expression != null)
            {
                var modelLambda = Expression.Lambda(memberExpression.Expression);
                var modelLambdaCompiled = (Func<object?>)modelLambda.Compile();
                var result = modelLambdaCompiled();
                if (result is null)
                    throw new ArgumentException("The provided expression must evaluate to a non-null value.");
                model = result;
            }
            else
                throw new ArgumentException($"The provided expression contains a {accessorBody.GetType().Name} which is not supported. {nameof(FieldIdentifier)} only supports simple member accessors (fields, properties) of an object.");
        }
    }
}
#pragma warning restore CS8622
#pragma warning restore CS8602
#nullable disable

and CSSBuilder

/// ============================================================
/// Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// ============================================================

using System.Collections.Generic;
using System.Text;
using System.Linq;

namespace Blazr.SPA.Components
{
    public class CSSBuilder
    {
        private Queue<string> _cssQueue = new Queue<string>();

        public static CSSBuilder Class(string cssFragment = null)
        {
            var builder = new CSSBuilder(cssFragment);
            return builder.AddClass(cssFragment);
        }

        public CSSBuilder()
        {
        }

        public CSSBuilder (string cssFragment)
        {
            AddClass(cssFragment);
        }

        public CSSBuilder AddClass(string cssFragment)
        {
            if (!string.IsNullOrWhiteSpace(cssFragment)) _cssQueue.Enqueue(cssFragment);
            return this;
        }

        public CSSBuilder AddClass(IEnumerable<string> cssFragments)
        {
            if (cssFragments != null)
                cssFragments.ToList().ForEach(item => _cssQueue.Enqueue(item));
            return this;
        }

        public CSSBuilder AddClass(string cssFragment, bool WhenTrue)
        {
            if (WhenTrue) return this.AddClass(cssFragment);
            return this;
        }

        public CSSBuilder AddClassFromAttributes(IReadOnlyDictionary<string, object> additionalAttributes)
        {
            if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
                _cssQueue.Enqueue(val.ToString());
            return this;
        }

        public CSSBuilder AddClassFromAttributes(IDictionary<string, object> additionalAttributes)
        {
            if (additionalAttributes != null && additionalAttributes.TryGetValue("class", out var val))
                _cssQueue.Enqueue(val.ToString());
            return this;
        }

        public string Build(string CssFragment = null)
        {
            if (!string.IsNullOrWhiteSpace(CssFragment)) _cssQueue.Enqueue(CssFragment);
            if (_cssQueue.Count == 0)
                return string.Empty;
            var sb = new StringBuilder();
            foreach(var str in _cssQueue)
            {
                if (!string.IsNullOrWhiteSpace(str)) sb.Append($" {str}");
            }
            return sb.ToString().Trim();
        }
    }
}

It looks like this in action:

enter image description here

4 Comments

If this doesn't help I'll delete the answer.
Thanks again for your input it really helped point me in the right direction. There was actually quite a bit of overlap with what EditForm/InputText offers so I didn't actually have to add much code and the part I was looking for was the private RenderFragment, its a new concept to me!
@CT14.IT. No problem, glad it helped. Mark your answer as the answer (I think you may have to wait a couple of days to do so) and give our two answers a +1.
Thanks again, will do (when I am allowed) !

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.