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)
Thank you