Disabling the Submit button in Blazor Validation
Blazor now has built-in validation and good documentation here. Some people want to disable the submit button until all fields have been successfully validated. How?
If you want to follow along with the code right in front of you, you can download the repo here.
One more thing: I've written this using Blazor Preview 6, maybe the future will make this a lot easier...
Attempt #1
So I have a project where I ask a customer to register, and I am using Blazor validation with EditForm
and InputText
components.
<h1>@Title</h1>
<EditForm @ref="editForm"
OnValidSubmit="@Submit"
Model="@Customer">
<DataAnnotationsValidator />
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="FirstName">Name:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Name" />
<ValidationMessage For="@(() => Customer.Name)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="LastName">Street:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Street" />
<ValidationMessage For="@(() => Customer.Street)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="Birthday">City:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.City" />
<ValidationMessage For="@(() => Customer.City)" />
</div>
</div>
<div class="form-group mb-0">
<button disabled="@isInvalid" type="submit" id="BtnRegister" class="@ButtonClass">
@ButtonTitle
</button>
</div>
</EditForm>
@isInvalid.ToJson()
@code {
[Parameter]
protected string Title { get; set; }
[Parameter]
protected string ButtonTitle { get; set; }
[Parameter]
protected string ButtonClass { get; set; }
[Parameter]
protected Customer Customer { get; set; }
[Parameter]
protected EventCallback<Customer> CustomerChanged { get; set; }
[Parameter]
protected EventCallback Submit { get; set; }
private EditForm editForm;
private bool isInvalid = false;
protected override void OnInit()
{
if( editForm?.EditContext == null)
{
Console.WriteLine("??? EditContext is null???");
} else
{
isInvalid = editForm.EditContext.Validate();
}
}
}
My index component has a customer and uses this CustomerEntry
component to edit.
@page "/"
<!-- Customer entry -->
<CustomerEntry1 Title="Please enter your details below"
ButtonTitle="Checkout"
ButtonClass="btn btn-primary"
Customer="@Customer"
Submit="@PlaceOrder" />
<!-- End customer entry -->
<hr />
<div>
@Customer.ToJson()
</div>
@code {
private Customer Customer { get; set; } = new Customer();
private void PlaceOrder()
{
Console.WriteLine("Placing order");
}
}
As my first attempt, I am trying to access the EditContext
from the EditForm
component. Then it becomes a simple matter of calling Validate
on the EditContext
. However, it is null
. Apparently, when you pass a Model
to the EditForm
the internal EditContext
does not get exposed...
Is this a bug, or a feature? Hmmm...
Attempt #2
Let us try creating the EditContext
ourself. Here is CustomerEntry
again.
<h1>@Title</h1>
<EditForm OnValidSubmit="@Submit"
EditContext="@editContext">
<DataAnnotationsValidator />
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="FirstName">Name:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Name" />
<ValidationMessage For="@(() => Customer.Name)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="LastName">Street:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Street" />
<ValidationMessage For="@(() => Customer.Street)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="Birthday">City:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.City" />
<ValidationMessage For="@(() => Customer.City)" />
</div>
</div>
<div class="form-group mb-0">
<button disabled="@isInvalid" type="submit" id="BtnRegister" class="@ButtonClass">
@ButtonTitle
</button>
</div>
</EditForm>
@isInvalid.ToJson()
@code {
[Parameter]
protected string Title { get; set; }
[Parameter]
protected string ButtonTitle { get; set; }
[Parameter]
protected string ButtonClass { get; set; }
[Parameter]
protected Customer Customer { get; set; }
[Parameter]
protected EventCallback<Customer> CustomerChanged { get; set; }
[Parameter]
protected EventCallback Submit { get; set; }
private EditContext editContext;
private bool isInvalid = true;
protected override void OnInit()
{
this.editContext = new EditContext(this.Customer);
this.editContext.OnFieldChanged += (sender, e) =>
{
isInvalid = !editContext.Validate();
this.StateHasChanged();
};
// isInvalid = !editContext.Validate();
}
}
Hey! This works. If you load this form, the submit button is disabled, and only disables when you complete the form without validation issues. If you want the button to be enabled in the beginning, simply uncomment the last line in OnInit
.
Attempt #3
But what if you do want to use the Model
property? Well, then you can capture the EditContext using another component. Let us start with CustomerEntry
again.
<h1>@Title</h1>
<EditForm OnValidSubmit="@Submit"
Model="@Customer">
<DataAnnotationsValidator />
<InputWatcher @ref="inputWatcher" FieldChanged="@FieldChanged" />
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="FirstName">Name:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Name" />
<ValidationMessage For="@(() => Customer.Name)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="LastName">Street:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Street" />
<ValidationMessage For="@(() => Customer.Street)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="Birthday">City:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.City" />
<ValidationMessage For="@(() => Customer.City)" />
</div>
</div>
<div class="form-group mb-0">
<button disabled="@isInvalid" type="submit" id="BtnRegister" class="@ButtonClass">
@ButtonTitle
</button>
</div>
</EditForm>
<div>
@Customer.ToJson()
</div>
<div>
@isInvalid.ToJson()
</div>
@code {
[Parameter]
protected string Title { get; set; }
[Parameter]
protected string ButtonTitle { get; set; }
[Parameter]
protected string ButtonClass { get; set; }
[Parameter]
protected Customer Customer { get; set; }
[Parameter]
protected EventCallback Submit { get; set; }
private InputWatcher inputWatcher;
private bool isInvalid = false;
private void FieldChanged(string fieldName)
{
Console.WriteLine($"*** {Customer.Name}");
isInvalid = !inputWatcher.Validate();
}
}
Notice the InputWatcher
? This is it:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
namespace BlazorValidation.Client
{
public class InputWatcher : ComponentBase
{
private EditContext editContext;
[CascadingParameter]
protected EditContext EditContext
{
get => editContext;
set
{
editContext = value;
EditContext.OnFieldChanged += async (sender, e) =>
{
await FieldChanged.InvokeAsync(e.FieldIdentifier.FieldName);
};
}
}
[Parameter]
protected EventCallback<string> FieldChanged { get; set; }
public bool Validate()
=> EditContext?.Validate() ?? false;
}
}
The InputWatcher
fetches the EditContext
as a cascading property and exposes the Validate
method. You can also bind to the FieldChanged
event where you call the Validate
method. It works. Period.
But there is one more thing I want to bring to your attention. Inside the CustomerEntry
component the customer updates (you can see the content of the Customer
below the submit button as simple JSON). But if you look at the Customer
in the Index
component you will see that it only updates when you submit. What if there is another component visualizing customer (as the JSON output below the horizontal ruler).
Attempt #4
Here is the last CustomerEntry
component of this blog post
<h1>@Title</h1>
<EditForm OnValidSubmit="@Submit"
Model="@Customer">
<DataAnnotationsValidator />
<InputWatcher @ref="inputWatcher" FieldChanged="@FieldChanged" />
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label" for="FirstName">Name:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Name" />
<ValidationMessage For="@(() => Customer.Name)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="LastName">Street:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.Street" />
<ValidationMessage For="@(() => Customer.Street)" />
</div>
</div>
<div class="form-group row mb-1">
<label class="col-sm-3 col-form-label"
for="Birthday">City:</label>
<div class="col-sm-9">
<InputText class="form-control"
@bind-Value="@Customer.City" />
<ValidationMessage For="@(() => Customer.City)" />
</div>
</div>
<div class="form-group mb-0">
<button disabled="@isInvalid" type="submit" id="BtnRegister" class="@ButtonClass">
@ButtonTitle
</button>
</div>
</EditForm>
<div>
@Customer.ToJson()
</div>
<div>
@isInvalid.ToJson()
</div>
@code {
[Parameter]
protected string Title { get; set; }
[Parameter]
protected string ButtonTitle { get; set; }
[Parameter]
protected string ButtonClass { get; set; }
[Parameter]
protected Customer Customer { get; set; }
[Parameter]
protected EventCallback<Customer> CustomerChanged { get; set; }
[Parameter]
protected EventCallback Submit { get; set; }
private InputWatcher inputWatcher;
private bool isInvalid = false;
private void FieldChanged(string fieldName)
{
Console.WriteLine($"*** {Customer.Name}");
isInvalid = !inputWatcher.Validate();
CustomerChanged.InvokeAsync(Customer);
}
}
Every time a field changes we invoke the CustomerChanged event. You would expect this to update the index, but no such luck... And I am using EventCallback<T>
...
Oh, wait. Two-way data-binding will update the UI when the target value changes:
<CustomerEntry3 Title="Please enter your details below"
ButtonTitle="Checkout"
ButtonClass="btn btn-primary"
@bind-Customer="@Customer"
Submit="@PlaceOrder" />