Introduction
Forms are part of almost every business application. Whether you are building a user registration page, customer management system, employee portal, or order management application, you will probably create several forms that contain similar fields and validation rules.
In a typical Blazor application, developers use EditForm with components such as InputText, InputNumber, InputSelect, and InputDate. This works well when building a small form. However, as the application grows, the same input fields, labels, validation messages, and styling start appearing in multiple components.
Maintaining these forms separately becomes difficult. If you need to change the appearance of a text field or update how validation messages are displayed, you may have to make the same changes in several files.
A better approach is to create reusable form components and use them across different pages.
Blazor provides component parameters, event callbacks, data binding, and validation support that make this possible. In this article, we will build a reusable text field component, connect it to an EditForm, and use it in a customer registration form.
Understanding Forms in Blazor
Blazor provides the EditForm component for handling forms. It connects form inputs with a data model and provides a way to validate user input before submitting the form.
Consider a simple customer model:
public class Customer
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Phone { get; set; } = string.Empty;
}A basic Blazor form can use this model directly:
<EditForm Model="@customer" OnValidSubmit="SaveCustomer">
<DataAnnotationsValidator />
<InputText @bind-Value="customer.Name" />
<InputText @bind-Value="customer.Email" />
<InputText @bind-Value="customer.Phone" />
<button type="submit">Save Customer</button>
</EditForm>The Model parameter connects the form to the customer object. OnValidSubmit calls the specified method only when validation succeeds, while DataAnnotationsValidator enables validation using attributes defined on the model.
This approach is simple, but the form still contains all its input definitions directly. If another page needs the same customer name or email field, the markup has to be written again.
Reusable components solve this problem by moving common input behavior into separate Razor components.
Creating a Reusable Text Field Component
Let's create a reusable component named FormTextField.razor.
The component will handle the label, text input, and validation message. Other pages will only need to provide the model property and label.
Create the following file:
Components/Shared/FormTextField.razor
<div class="form-group mb-3">
<label for="@Id">@Label</label>
<InputText id="@Id"
class="form-control"
Value="@Value"
ValueChanged="@ValueChanged"
ValueExpression="@ValueExpression"
placeholder="@Placeholder" />
<ValidationMessage For="@ValueExpression" />
</div>
@code {
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string Placeholder { get; set; } = string.Empty;
[Parameter]
public string Id { get; set; } = Guid.NewGuid().ToString("N");
[Parameter]
public string? Value { get; set; }
[Parameter]
public EventCallback<string?> ValueChanged { get; set; }
[Parameter]
public Expression<Func<string?>>? ValueExpression { get; set; }
}The component uses three parameters to support Blazor's two-way binding.
Value contains the current input value. ValueChanged notifies the parent component when the user changes that value. ValueExpression identifies the model property associated with the input.
The ValueExpression parameter is important for validation. It allows Blazor to associate the input with the correct field in the form's EditContext.
Without this connection, validation messages may not appear against the expected model property.
The component uses the existing InputText component instead of a normal HTML input. This means it continues to participate in Blazor's form handling and validation system.
Make sure to include the required namespaces in the component's imports:
@using System.Linq.Expressions
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.FormsYou can place these imports in _Imports.razor if they are used across multiple components.
Adding Validation to the Customer Model
A reusable input component becomes more useful when it works with model validation.
Update the customer model to include data annotations:
using System.ComponentModel.DataAnnotations;
public class Customer
{
[Required(ErrorMessage = "Customer name is required.")]
[StringLength(50, ErrorMessage = "Name cannot exceed 50 characters.")]
public string Name { get; set; } = string.Empty;
[Required(ErrorMessage = "Email address is required.")]
[EmailAddress(ErrorMessage = "Enter a valid email address.")]
public string Email { get; set; } = string.Empty;
[Required(ErrorMessage = "Phone number is required.")]
[RegularExpression(
@"^[0-9]{10}$",
ErrorMessage = "Enter a valid 10-digit phone number."
)]
public string Phone { get; set; } = string.Empty;
}Each attribute defines a validation rule.
The Required attribute prevents empty values, StringLength limits the customer name, and EmailAddress checks the email format.
The regular expression checks that the phone number contains exactly ten digits. This rule is suitable only if the application specifically expects that phone number format. Applications supporting international numbers should use a more flexible validation approach.
Keeping these rules in the model means the same validation logic can be used by different forms.
Using Reusable Components in an EditForm
Now create a customer registration page named CustomerForm.razor.
@page "/customers/create"
@using YourApp.Models
@using YourApp.Components.Shared
<h3>Create Customer</h3>
<EditForm Model="@customer"
OnValidSubmit="@SaveCustomer">
<DataAnnotationsValidator />
<ValidationSummary />
<FormTextField Label="Customer Name"
Placeholder="Enter customer name"
@bind-Value="customer.Name" />
<FormTextField Label="Email Address"
Placeholder="Enter email address"
@bind-Value="customer.Email" />
<FormTextField Label="Phone Number"
Placeholder="Enter phone number"
@bind-Value="customer.Phone" />
<button type="submit" class="btn btn-primary">
Save Customer
</button>
</EditForm>
@if (!string.IsNullOrEmpty(message))
{
<p>@message</p>
}
@code {
private Customer customer = new();
private string message = string.Empty;
private async Task SaveCustomer()
{
// Save the customer through the application service.
message = "Customer information saved successfully.";
await Task.CompletedTask;
}
}
Join the conversation! Your thoughts help the community grow.