Input Text Component

In my last post I decided on a few features for a text input component I’ll name InputTextBCA. I plan to append most if not all the components I design with BCA for Blazor Component Authority. I’ve had the experience of trying to use two different libraries of components on the same page and ran into a naming ambiguity error which forced me to use a fully qualified name. I didn’t like doing that. It seemed a bit noisy to me. So to avoid that issue in the future I’ll make my component names unique with the BCA tag. 

While preparing to design the InputText component I took a look at the parameters available in a few other Blazor component libraries as well as the attributes of the HTML input control and noticed that there are a variety of names used for the same purpose. For example the event name used for when a change occurs to the input such as a key press. I found the following names used for this event, onchange, ValueChanged, and Changed. It seems to me we could all benefit from a standard to follow. 

I propose we follow the HTML input attribute names which are well documented. So for the example above the event name is onchange. Of course we will still need to stick to the naming convention that Blazor uses for implementing two way binding. That is the name of the parameter with the word Changed appended to it for the EventCallback parameter that enables the binding from the child to the parent.

Here’s the features from the previous post that I decided to include in the text input, plus a few more I thought of along the way.

  1. A green outline to indicate the value was modified
  2. A the red outline to indicate the input value failed validation
  3. A validation failure message below the input
  4. A label above the input
  5. Can be used outside of an EditForm if validation is not required
  6. Provide OnChanged event that can be used while using two way binding
  7. ClassInput parameter for custom styling of the input element (default form-control)
  8. ClassLabel parameter for custom styling of the label element
  9. ClassDiv parameter for custom styling of the div wrapping all the Razor markup
  10. ClassValid parameter for custom styling of the input with a valid value (default valid)
  11. ClassModified parameter for custom styling of the input with a modified value (default modified)
  12. ClassInvalid parameter for custom styling of the input with a invalid value (default invalid)
  13. ClassValidationMessage parameter for custom styling of the validation message 
  14. Id parameter to possibly help make automated testing less fragile
  15. Provide default values where appropriate to make utilizing the component as simple as possible.

Here’s the result

Using this Razor Markup

<EditForm @ref=MyCar.EditForm Model="MyCar">
    <DataAnnotationsValidator />
    <div class="row">
        <InputTextBCA Label="MAKE" 
                      @bind-Value=MyCar.Make 
                      For="Make" 
                      ClassDiv="col-3" />
        <InputTextBCA Label="MODEL" 
                      @bind-Value=MyCar.Model 
                      For="Model" 
                      ClassDiv="col-3" />
        <InputTextBCA Label="VIN" 
                      @bind-Value=MyCar.VIN 
                      For="VIN" 
                      ClassDiv="col-4" />
        <div class="col-2">
            <button @onclick=@(() => Save(MyCar)) 
                    class="btn btn-primary mt-4">
                    SAVE
            </button>
        </div>
    </div>
</EditForm>

For comparison this is the equivalent markup using the components that come with Blazor.

<EditForm Model="MyCar">
    <DataAnnotationsValidator />
    <div class="row">
        <div class="col-3">
            <label>
                MAKE
            </label>
            <InputText @bind-Value=MyCar.Make
                       class="form-control">
            </InputText>
            <ValidationMessage 
                For="() => MyCar.Make" />
        </div>     
        <div class="col-3">
            <label>
                MODEL
            </label>
            <InputText @bind-Value=MyCar.Model
                       class="form-control">
            </InputText>
            <ValidationMessage 
                For="() => MyCar.Model" />
        </div>
        <div class="col-4">
            <label>
                VIN
            </label>
            <InputText @bind-Value=MyCar.VIN
                       class="form-control">
            </InputText>
            <ValidationMessage 
                For="() => MyCar.VIN" />
        </div>
        <div class="col-3" style="width: 100px;">
            <button type="submit"
                    class="btn btn-primary mt-3">
                SAVE
            </button>
        </div>
    </div>
</EditForm>

You can see that using the InputTextBCA component makes the markup considerably more compact. Which I believe is easier to read and not as noisy.

Here’s the InputTextBCA component markup

@inherits InputTextBaseBCA

<div class="@ClassDiv">
    @if (!string.IsNullOrWhiteSpace(Label))
    {
        <label class="@ClassLabel"
               for="@Id">
               @Label
        </label>
    }
    
    <input @bind-value=@Value 
           class="form-control @ClassInput @ValidationResultClass"
           type="text" 
           placeholder="@Placeholder" 
           id="@Id" 
           title="@Title"/>

    @foreach (var message in ValidationMessages)
    {
        <h6 class="@ClassValidationMessage"
            style="margin-top:3px;">
            @message
        </h6>
    }
</div>

InputTextBCA inherits from InputTextBaseBCA. I put the code in a separate class (InputTextBaseBCA) so that it can be used with other components.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;

namespace BlazorComponentAuthorityLibrary
{
    public class InputTextBaseBCA : ComponentBase
    {
        private string? originalValue { get; set; }
        private string? value { get; set; }

        /// <summary>
        /// The string value of the input
        /// </summary>
        [Parameter]
        public string? Value
        {
            get { return value; }

            set
            {
                if (this.value != value)
                {
                    this.value = value;

                    if (ValueChanged.HasDelegate)
                    {
                        ValueChanged.InvokeAsync(value);                        
                    }

                    if (OnChanged.HasDelegate)
                    {
                        OnChanged.InvokeAsync(value);
                    }

                    if (EditContext != null && For != null)
                    {
                        var fieldIdentifier = EditContext.Field(For);
                        EditContext.NotifyFieldChanged(fieldIdentifier);
                    }
                }
            }
        }

        /// <summary>
        /// A callback method used to update the property bound to the Value parameter.
        /// </summary>
        [Parameter]
        public EventCallback<string> ValueChanged { get; set; }

        /// <summary>
        /// A callback method that is invoked when the Value changes.
        /// </summary>
        [Parameter]
        public EventCallback<string> OnChanged { get; set; }

        /// <summary>
        /// A value shown in the input when the input is empty or null.
        /// </summary>
        [Parameter]
        public string Placeholder { get; set; } = string.Empty;

        /// <summary>
        /// The input Id.
        /// Can be used to uniquely identify instances of the component.
        /// Helps with making UI testing less brittle.
        /// </summary>
        [Parameter]
        public string Id { get; set; } = string.Empty;

        /// <summary>
        /// The title or tooltip to show when the user hovers over the input.
        /// </summary>
        [Parameter]
        public string Title { get; set; } = string.Empty;

        [CascadingParameter]
        private EditContext? EditContext { get; set; }

        /// <summary>
        /// The name of the EditContext model property bound to this input.
        /// Required to display the model property validation messages.
        /// </summary>
        [Parameter]
        public string? For { get; set; }

        /// <summary>
        /// The label to be displayed.
        /// </summary>
        [Parameter]
        public string Label { get; set; } = string.Empty;

        /// <summary>
        /// The class to be applied to the input element.
        /// </summary>
        [Parameter]
        public string ClassInput { get; set; } = string.Empty;

        /// <summary>
        /// The class to be applied to the label element.
        /// </summary>
        [Parameter]
        public string ClassLabel { get; set; } = string.Empty;

        /// <summary>
        /// The class to be applied to the div that wraps the component.
        /// </summary>
        [Parameter]
        public string ClassDiv { get; set; } = string.Empty;

        /// <summary>
        /// The class to be applied to the input when the input value is valid.
        /// </summary>
        [Parameter]
        public string ClassValid { get; set; } = "valid";

        /// <summary>
        /// The class to be applied to the input when the input value has been modified.
        /// </summary>
        [Parameter]
        public string ClassModified { get; set; } = "valid modified";

        /// <summary>
        /// The class to be applied to the input when the input value is invalid.
        /// </summary>
        [Parameter]
        public string ClassInvalid { get; set; } = "invalid";

        protected string ValidationResultClass { get; set; } = string.Empty;

        /// <summary>
        /// The class to be applied to the validation message.
        /// </summary>
        [Parameter]
        public string ClassValidationMessage { get; set; } = "validation-message";

        /// <summary>
        /// The list of validation messages.
        /// </summary>
        public List<string> ValidationMessages { get; set; } = new List<string>();

        protected override void OnInitialized()
        {
            if (EditContext != null && !string.IsNullOrWhiteSpace(For))
            {
                EditContext.OnFieldChanged += HandleFieldChanged;
                EditContext.OnValidationStateChanged += HandleValidationStateChanged;
            }

            originalValue = Value;
            ValidationResultClass = ClassValid;
            base.OnInitialized();
        }

        private void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
        {
            GetApplicableValidationMessages();
        }

        private void HandleValidationStateChanged(object? sender, ValidationStateChangedEventArgs e)
        {
            GetApplicableValidationMessages();
        }

        private void GetApplicableValidationMessages()
        {
            if (EditContext == null) { return; }

            var fieldIdentifier = EditContext.Field(For);
            var messages = EditContext.GetValidationMessages(fieldIdentifier).ToList();
            if (messages.Count == 0)
            {
                ValidationMessages.Clear();
                var isModified = EditContext.IsModified(fieldIdentifier);
                
                if (isModified && Value != originalValue)
                {
                    ValidationResultClass = ClassModified;
                }
                else
                {
                    ValidationResultClass = ClassValid;
                }
            }
            else
            {
                ValidationResultClass = ClassInvalid;
                ValidationMessages = messages;
            }
            StateHasChanged();
        }

        private void Dispose()
        {
            if (EditContext == null) { return; }
            EditContext.OnFieldChanged -= HandleFieldChanged;
            EditContext.OnValidationStateChanged -= HandleValidationStateChanged;
        }
    }
}

The inline version looks like this

using this markup

<EditForm Model="MyCar">
    <DataAnnotationsValidator />
    <div class="row">
        <InputTextInlineBCA Label="MAKE" 
                            @bind-Value=MyCar.Make
                            For="Make" ClassDiv="col-4" />
        <InputTextInlineBCA 
                            Label="MODEL" 
                            @bind-Value=MyCar.Model
                            For="Model" ClassDiv="col-4" />
        <InputTextInlineBCA 
                            Label="VIN" 
                            @bind-Value=MyCar.VIN
                            For="VIN" ClassDiv="col-4" />
        <div class="col-3" style="width: 100px;">
            <button type="submit" 
                    class="btn btn-primary mt-3">
                SAVE
            </button>
        </div>
    </div>
</EditForm>

Here’s the InputTextInlineBCA component mark up which also inherits from InputTextBaseBCA.

@inherits InputTextBaseBCA

<div class="@ClassDiv">
    
    <div class="input-group">
        <div class="input-group-text @ClassLabel @ValidationResultClass">
            @Label
        </div>
        <input @bind-value=@Value
               class="form-control @ClassInput @ValidationResultClass"
               type="text"
               placeholder="@Placeholder"
               id="@Id"
               title="@Title" />
    </div>    

    @foreach (var message in ValidationMessages)
    {
        <h6 class="@ClassValidationMessage"
            style="margin-top:3px;">
            @message
        </h6>
    }
</div>


Validation requiring database access can be accomplished with a custom validation attribute. Here’s an example.

public class IsUniqueVinAttribute : ValidationAttribute
    {
        public override bool IsValid(object? value)
        {
            string? vin = value?.ToString()?.ToUpper();

            if (vin == null)
            {
                return true;
            }
            

            if (CarService.VIN_Exists((string)vin))
            {
                return false; 
            }

            return true;
        }
    }

Then decorate the Car VIN property with the IsUniqueVinAttribute so that when the EditContext performs the validation it will pass the value of the VIN property to the IsUniqueVinAttribute IsValid method.

[Required]
        [IsUniqueVinAttribute(ErrorMessage = "VIN number already exist in the database")]
        public string? VIN { get; set; }

These are, as I see it, the advantages to incorporating several commonly used elements into a component.

  1. Easier to read and maintain markup and code
  2. Reduces errors
  3. Improves production velocity
  4. Less typing
  5. Fewer implementation details to remember

I really enjoyed putting these components together and it would be great if someone finds them useful. Thanks for reading.

Site Footer

Sliding Sidebar