In my last post, we looked at how we could build custom input components on top of InputBase
. Using InputBase
saves us loads of extra work by managing all of the interactions with the EditForm
component and the validation system, which is excellent.
However, sometimes, there are situations where we want or need to have a bit more control over how our input components behave. In this post, we are going to look at how we can build input components from scratch.
Why build from scratch?
When you consider all the functionality that we get from Blazor's out-of-the-box input components. Plus, the ability to make customisations using the InputBase
class that we looked at last time. Why would we want to build input components from scratch?
The biggest reason I've found so far is the ability to use input components outside of an EditForm
component. Any input component which uses InputBase has to be inside of an EditForm
component; otherwise, an exception is thrown. That's because of a check in the OnParametersSet
method of InputBase
. It checks for an EditContext
which is cascaded down via the EditForm
component.
if (CascadedEditContext == null)
{
throw new InvalidOperationException($"{GetType()} requires a cascading parameter " + $"of type {nameof(Forms.EditContext)}. For example, you can use {GetType().FullName} inside " + $"an {nameof(EditForm)}.");
}
Building from scratch is also beneficial if you're looking to have total control over how your component acts. By creating everything yourself, you'll be able to tailor every detail to work precisely the way you want. For example, you could create an EditForm
replacement, and that could require you to build custom input components.
Building from scratch
We're going to build a simple text input component which can be used both inside or outside an EditForm
component. Text inputs are quite useful components to be able to use both inside a form for any text-based entry. But are equally useful outside of a form for things like search boxes where validation isn't necessarily a concern.
<input value="@Value" @oninput="HandleInput" />
@code {
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
private async Task HandleInput(ChangeEventArgs args)
{
await ValueChanged.InvokeAsync(args.Value.ToString());
}
}
This is the basic setup of our CustomInputText
component. We have set up a couple of parameters, Value
and ValueChanged
. These allow us to use Blazor's bind
directive when consuming the control. We've hooked onto the input controls oninput
event, and every time it fires the HandleInput
event invokes the ValueChanged
EventCallback
to update the value for the consumer.
Working as part of EditForm
For an input component to work with EditForm, it has to integrate with EditContext
. EditContext
is the brain of a Blazor form; it holds all of the metadata regarding the state of the form. Things like whether a field has been modified. Is it valid? As well as a collection of all of the current validation messages.
EditContext
is also responsible for raising events to signal that field values have been changed, or that an attempt has been made to submit the form. This triggers the validation aspect of the form.
Integrating with EditContext
To integrate with EditContext, we need to add a CascadingParameter
to our component requesting it; then we need to create a FieldIdentifier
.
The FieldIdentifier
class uniquely identifies a specific field or property in the form. To create an instance, we need to pass in an expression which identifies the field our component is handling. To get this expression, we can add another parameter to our component called ValueExpression
. Blazor populates this expression for us based on a convention in a similar way to two-way binding using Value
and ValueChanged
.
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
Now we have an expression we can create an instance of FieldIdentifier
; we'll do this in the OnInitialized
life cycle method.
protected override void OnInitialized()
{
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
}
We need to tell the EditContext
when the value of our field has been updated. This will trigger any validation logic that needs to run against our field. We do this by calling the NotifyFieldChanged
method on the EditContext
.
private async Task HandleInput(ChangeEventArgs args)
{
await ValueChanged.InvokeAsync(args.Value.ToString());
CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
}
Once the field has been validated it will be marked as either valid or invalid. We can use this value to assign CSS classes to the component and style it appropriately. To access these values we can use the following code.
private string _fieldCssClasses => _editContext?.FieldCssClass(_fieldIdentifier) ?? "";
This going to set the _fieldCssClasses
field to some combination of modified
valid
or invalid
, depending on the fields current state.
The final component looks like this.
<input class="_fieldCssClasses" value="@Value" @oninput="HandleInput" />
@code {
private FieldIdentifier _fieldIdentifier;
private string _fieldCssClasses => CascadedEditContext?.FieldCssClass(_fieldIdentifier) ?? "";
[CascadingParameter] private EditContext CascadedEditContext { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
protected override void OnInitialized()
{
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
}
private async Task HandleInput(ChangeEventArgs args)
{
await ValueChanged.InvokeAsync(args.Value.ToString());
CascadedEditContext?.NotifyFieldChanged(_fieldIdentifier);
}
}
Working without EditForm
Actually, we've already covered this one. You may have noticed on the last code snippet that we used the null-conditional operator (?.
) when calling the NotifyFieldChanged
method. The reason for this is that if the EditContext
is null
, then the method won't be called.
Why would the EditContext
be null
? If the control wasn't inside of an EditForm
component. That simple check will allow the control to work outside of an EditForm
component without any issue.
What are the costs?
When we use this component without the EditForm
component we will no longer be able to use the standard validation mechanisms. Depending on your use case this may or may not matter.
For the use cases I've had, it doesn't matter, things such as site searches or date pickers, things where I can use default values or not have to care. You could, of course, deal with this manually if you choose. You're in complete control of the component after all.
For example, we could add a Required
parameter to the component. When this is true, we can check if there is an EditContext
. If there isn't, we can set a private variable to show an error message if the current value is empty.
<input class="_fieldCssClasses" value="@Value" @oninput="HandleInput" />
@if (_showValidation)
{
<div class="validation-message">You must provide a name</div>
}
@code {
private FieldIdentifier _fieldIdentifier;
private string _fieldCssClasses => CascadedEditContext?.FieldCssClass(_fieldIdentifier) ?? "";
private bool _showValidation;
[CascadingParameter] private EditContext CascadedEditContext { get; set; }
[Parameter] public string Value { get; set; }
[Parameter] public EventCallback<string> ValueChanged { get; set; }
[Parameter] public Expression<Func<string>> ValueExpression { get; set; }
[Parameter] public bool Required { get; set; }
protected override void OnInitialized()
{
_fieldIdentifier = FieldIdentifier.Create(ValueExpression);
}
private async Task HandleInput(ChangeEventArgs args)
{
await ValueChanged.InvokeAsync(args.Value.ToString());
if (CascadedEditContext != null)
{
CascadedEditContext.NotifyFieldChanged(_fieldIdentifier);
}
else if (Required)
{
_showValidation = string.IsNullOrWhiteSpace(args.Value.ToString());
}
}
}
If we don't want the error message to be hardcoded, that's cool too; we can add a parameter for the error message so that it can be passed in. The point here is that you can customise the behaviour as much as you like based on your needs.
Summary
In this post, we've looked at how we can build bespoke input components that work inside and outside of the EditForm
component. We started by looking at why we would want to do this in the first place. Then we looked at how to integrate with the built in forms and validation system of Blazor. As well as how to make the component work without that system. Finally, we talked about some of the trade off of working outside of EditForm
.
Top comments (0)