DEV Community

Chris Sainty
Chris Sainty

Posted on • Originally published at chrissainty.com on

Creating a Custom Validation Message Component for Blazor Forms

Creating a Custom Validation Message Component for Blazor Forms

As some of you may know, I'm a big fan of Tailwind CSS. If you've not heard of Tailwind before then please checkout my previous posts about it which can be found here and here. The reason I mention this is because when I was using it on a recent Blazor project, I hit a bit of a snag. I wanted to style my validation messages using Tailwinds utility classes, but I couldn't add them to the component. This is because the ValidationMessage component adds a hard-coded class which can't be added to or overriden.

builder.OpenElement(0, "div");
builder.AddMultipleAttributes(1, AdditionalAttributes);
builder.AddAttribute(2, "class", "validation-message");
builder.AddContent(3, message);
builder.CloseElement();

The full source can be viewed here, but as you can see in the snippet above, while the component supports passing additional attributes, the component will always override any class attribute that is supplied.

I could've added a wrapper div around each usage of ValidationMessage and added my classes there, but that felt like a clunky solution. I've also seen several people in the community asking about customising the output of the ValidationSummary component. So I thought this would be a good opportunity to come up with a better way.

In this post, I'm going to show how you can create a ValidationMessage component with customisable UI. I'll start by showing a more simplistic approach and then show a more robust and reusable solution.

How does ValidationMessage work?

Before we get into the solutions, I wanted to quickly cover how the standard ValidationMessage works.

The default component is pretty compact weighing in at about 100 lines of code. It accepts a cascaded EditContext and adds an event handler for the OnValidationStateChanged event. All this handler does is call StateHasChanged whenever the event fires.

It also creates a FieldIdentifier based on whichever model property has been specified via the For parameter. This is then used when calling the GetValidationMessages method on the EditContext. This method will return all of the current validation error messages for the given FieldIdentifier. Any messages returned are then rendered.

In summary, the componet does three things:

  • Listens for changes to the validation state
  • Retrieves any validation messages for the model property specified
  • Displays the messages on screen

Now we understand what the original component does, let's move on to the solutions.

Creating a simple replacement

The first option, which was pretty quick to build, was largly a modified version of the original component.

The default component caters for a lot of potential scenarios, things like dynamically changing EditContext's or updates to the For parameter. Therefore there's a lot of code which is checking and caching values to support this and be as efficient as possible.

However, my use case was very straightforward, just a standard form which wouldn't have anything dynamically changing. This mean't that I could do away with a good amount of code. The result is the following.

@using System.Linq.Expressions

@typeparam TValue
@implements IDisposable

@foreach (var message in EditContext.GetValidationMessages(_fieldIdentifier))
{
    <div class="@Class">
        @message
    </div>
}

@code {
    [CascadingParameter] private EditContext EditContext { get; set; }

    [Parameter] public Expression<Func<TValue>> For { get; set; }
    [Parameter] public string Class { get; set; }

    private FieldIdentifier _fieldIdentifier;

    protected override void OnInitialized()
    {
        _fieldIdentifier = FieldIdentifier.Create(For);
        EditContext.OnValidationStateChanged += HandleValidationStateChanged;
    }

    private void HandleValidationStateChanged(object o, ValidationStateChangedEventArgs args) => StateHasChanged();

    public void Dispose()
    {
        EditContext.OnValidationStateChanged -= HandleValidationStateChanged;
    }
}

I implemented the same fundamental behavor as the original component, I created a FieldIdentifier based on the model property defined via the For parameter. Registered a handler for the OnValidationStateChanged event on EdiContext. I also unregistered it to avoid any memory leaks by implementing IDisposable.

In my markup, I then output any validation messages as per the original component, but with one simple difference. I removed the hard-coded CSS applied and added a Class parameter. Now I could provide any classes I wanted to apply on a case by case basis. Here's an example of usage.

<CustomValidationMessage For="@(() => _model.FirstName)"
                         Class="mt-2 sm:ml-4 font-semibold text-red-600" />

This implementation resolved the original issue, I could now use my Tailwind CSS classes to style my validation messages without any issues. Job done, right?

For my immediate problem, yes. But it got me thinking about some of those comments from the community I mentioned in the introduction. In those comments, developers were asking about changing the HTML that was rendered, not just adding custom CSS classes. This solution doesn't help with that problem. This lead me to create my second solution.

ValidationMessageBase for ultimate customisation

I really love the approach the Blazor team took with building the input components for forms. Providing us with InputBase<T> is great as we can focus on building custom UI, which is what needs to be changed in 99% of cases, while the boilerplate of integrating with the form and validation system is taken care of.

Wouldn't it be great if there was something like that for validation messages as well...?

Creating ValidationMessageBase

By creating a base class for validation messages, as per the design of the input components, it would give developers the freedom to tweak the UI output to their exact needs. Here's the code.

public class ValidationMessageBase<TValue> : ComponentBase, IDisposable
{
    private FieldIdentifier _fieldIdentifier;

    [CascadingParameter] private EditContext EditContext { get; set; }
    [Parameter] public Expression<Func<TValue>> For { get; set; }
    [Parameter] public string Class { get; set; }

    protected IEnumerable<string> ValidationMessages => EditContext.GetValidationMessages(_fieldIdentifier);

    protected override void OnInitialized()
    {
        _fieldIdentifier = FieldIdentifier.Create(For);
        EditContext.OnValidationStateChanged += HandleValidationStateChanged;
    }

    private void HandleValidationStateChanged(object o, ValidationStateChangedEventArgs args) => StateHasChanged();

    public void Dispose()
    {
        EditContext.OnValidationStateChanged -= HandleValidationStateChanged;
    }
}

This is essentially the logic from the code block in the first solution, except I've added a property which returns the current validation messages instead of calling the GetValidationMessages method directly on the EditContext. This is purely to make the developer experience a little nicer when implementing markup for the validation messages.

With this base class I can implement the same markup as I had for the first solutions really easily.

@typeparam TValue
@inherits ValidationMessageBase<TValue>

@foreach (var message in ValidationMessages)
{
    <div class="@Class">
        @message
    </div>
}

And if I want to implement something different in a future project all I need to do is create a new derived component.

@typeparam TValue
@inherits ValidationMessageBase<TValue>

@if (ValidationMessages.Any())
{
    <ul class="validation-errors">
        @foreach (var message in ValidationMessages)
        {
            <li class="validation-error-message">
                @message
            </li>
        }
    </ul>
}

Summary

In this post, I've show a limitation with the default ValidationMessage component which comes with Blazor, specifically, the inability to customise the markup it produces. I've shown two potential solutions.

The first is a modified version of the original component. The hard-coded styling was removed and replaced with a Class parameter allowing CSS classes to be specified per usage.

The second solution was based on the design of Blazors input components. A base class was used to abstract away the boilerplate code, allowing developers to focus on creating the specific markup they require, for ultimate flexability.

Top comments (1)

Collapse
 
theonlybeardedbeast profile image
TheOnlyBeardedBeast

Thank you