Out of the box, Blazor gives us some great components to get building forms quickly and easily. The EditForm
component allows us to manage forms, coordinating validation and submission events. There’s also a range of built-in input components which we can take advantage of:
InputText
InputTextArea
InputSelect
InputNumber
InputCheckbox
InputDate
And of course, we wouldn’t get very far without being able to validate form input, and Blazor has us covered there as well. By default, Blazor uses the data annotations method for validating forms, which if you’ve had any experience developing ASP.NET MVC or Razor Page applications, will be quite familiar.
Out of the many things I love about Blazor, the ability to customise things which don’t quite suit your tastes or needs is one of my favourites! And forms are no exception. I’ve previously blogged about how you can swap out the default data annotations validation for FluentValidation. In this post, I’m going to show you how you can create your own input components using InputBase
as a starting point.
Some issues when building real-world apps
The Blazor team have provided us with some great components to use out of the box that cover many scenarios. But when building real-world applications, we start to hit little problems and limitations.
Lots and lots of repeated code
Most applications, especially line of business applications, require quite a few forms. These often have a set style and layout throughout the application. When using the built-in input components, this means things can get verbose and repetitive quite quickly.
<EditForm Model="NewPerson" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label for="firstname">First Name</label>
<InputText @bind-Value="NewPerson.FirstName" class="form-control" id="firstname" />
<ValidationMessage For="NewPerson.FirstName" />
</div>
<div class="form-group">
<label for="lastname">Last Name</label>
<InputText @bind-Value="NewPerson.LastName" class="form-control" id="lastname" />
<ValidationMessage For="NewPerson.LastName" />
</div>
<div class="form-group">
<label for="occupation">Occupation</label>
<InputText @bind-Value="NewPerson.Occupation" class="form-control" id="occupation" />
<ValidationMessage For="NewPerson.Occupation" />
</div>
<button type="submit">Save</button>
</EditForm>
The more significant issue, however, is maintenance. This code is using Bootstrap for layout and styling, but what happens if that changed and we moved to a different CSS framework? Or for some reason stopped using a CSS framework altogether and wrote our own CSS? We’d have to go everywhere we had a form in the application and update the code. Having experienced this first hand, I can safely say this isn’t fun.
A solution: Building custom input components
The approach my team and I have taken at work is to create custom input components which suit our applications needs. By doing this, we’ve greatly reduce the amount of code we write, while also making updates to styling and functionality much quicker and simpler.
All our form components can have an optional label, input control and validation message. If we didn’t use our custom components, the code would look like this.
<!-- Control with label -->
<div class="form-control-wrapper">
<label class="form-control-label" for="catalogue">Catalogue</label>
<InputText class="form-control" id="catalogue" @bind-Value="Form.Catalogue" />
<div class="form-control-validation">
<ValidationMessage For="@(() => Form.Catalogue)" />
</div>
</div>
<!-- Control without label -->
<div class="form-control-wrapper">
<InputText class="form-control" id="client" @bind-Value="Form.Client" />
<div class="form-control-validation">
<ValidationMessage For="@(() => Form.Client)" />
</div>
</div>
But with our custom components the same functionality is achieved using far less code.
<!-- Control with label -->
<SwInputText Label="Catalogue" @bind-Value="Form.Catalogue" ValidationFor="@(() => Form.Catalogue)" />
<!-- Control without label -->
<SwInputText @bind-Value="Form.Client" ValidationFor="@(() => Form.Client)" />
Now if we want to update the styling of the SwInputText
component, we can do it in one place, and the whole of our app is updated.
How do we do this?
All of the standard input components in Blazor inherit from a single base class called InputBase
. This class handles all of the heavy lifting when it comes to validation by integrating with EditContext
. It also manages the value binding boilerplate by exposing a Value
parameter of type T
. Hence whenever you use one of the build-in form controls you bind to it like this, @bind-Value="myForm.MyValue"
.
Building on InputBase
We didn’t want to recreate all the integration with the built-in form component. So we took InputBase
as a starting point and built our own components on top of it. This is what the code looks like for our SwInputText
component.
@using System.Linq.Expressions
@inherits InputBase<string>
<div class="form-control-wrapper">
@if (!string.IsNullOrWhiteSpace(Label))
{
<label class="form-control-label" for="@Id">@Label</label>
}
<input class="form-control @CssClass" id="@Id" @bind="@CurrentValue" />
<div class="form-control-validation">
<ValidationMessage For="@ValidationFor" />
</div>
</div>
@code {
[Parameter, EditorRequired] public Expression<Func<string>> ValidationFor { get; set; } = default!;
[Parameter] public string? Id { get; set; }
[Parameter] public string? Label { get; set; }
protected override bool TryParseValueFromString(string? value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = null;
return true;
}
}
The SwInputText
component inherits from InputBase
and the only real work we have to do is provide an implementation for the TryParseValueFromString
method and a few additional parameters.
Because all but one (InputCheckbox) of the built-in input components bind to string
representations of the bound value internally. This method is required to convert the string value back to whatever the original type was. In our case, we’re only binding to string
s so it’s just a case of setting the result
parameter to equal the value
parameter and we’re done.
The majority of the effort has gone into the markup side of the component. This is where we’re encapsulating our UI design and the logic for showing a label or not.
Summary
In this post, we talked about the issue of maintenance and maintainability of forms in real-world Blazor applications. As a solution, we looked at building our own Input component, using InputBase
as a starting point. This allowed us to encapsulate the UI design in a single place making future maintenance much easier.