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;
}
}Notice that the page does not contain separate InputText and ValidationMessage elements for every field.
Instead, each field is created using FormTextField.
The @bind-Value syntax connects the component to the corresponding property. Blazor automatically supplies the value, change callback, and expression needed for binding and validation.
When the user submits the form, DataAnnotationsValidator checks the model. If a validation rule fails, OnValidSubmit is not executed.
In a real application, replace the placeholder in SaveCustomer with a call to your customer service or repository.
For example:
private async Task SaveCustomer()
{
await CustomerService.CreateAsync(customer);
message = "Customer information saved successfully.";
}The service should handle database operations, duplicate customer checks, and other business rules.
Creating Reusable Number and Dropdown Components
Once the text field component is working, the same idea can be applied to other input types.
For example, a reusable number field can use InputNumber:
@using System.Linq.Expressions
@using Microsoft.AspNetCore.Components.Forms
<div class="form-group mb-3">
<label for="@Id">@Label</label>
<InputNumber TValue="int"
id="@Id"
class="form-control"
Value="@Value"
ValueChanged="@ValueChanged"
ValueExpression="@ValueExpression" />
<ValidationMessage For="@ValueExpression" />
</div>
@code {
[Parameter]
public string Label { get; set; } = string.Empty;
[Parameter]
public string Id { get; set; } = Guid.NewGuid().ToString("N");
[Parameter]
public int Value { get; set; }
[Parameter]
public EventCallback<int> ValueChanged { get; set; }
[Parameter]
public Expression<Func<int>>? ValueExpression { get; set; }
}This component can be used for quantities, product counts, or other integer fields.
For dropdowns, create another reusable component using InputSelect. Its parameters can include the selected value, available options, and the corresponding validation expression.
It is generally better to create separate components for different input types rather than trying to handle every possible input through one complicated component.
When to Use Reusable Form Components
Reusable components are especially helpful when several pages share the same form structure.
Scenario | Recommended approach |
|---|---|
Small form used on one page | Direct |
Similar fields across multiple pages | Reusable input components |
Shared validation rules | Data annotations or reusable schemas |
Complex multi-section forms | Separate child components |
Dynamic form fields | Component-based rendering |
Custom validation requirements | Custom validation attributes or |
Not every input needs to become a separate component. If a field is used only once and has no reusable behavior, keeping it directly inside the page may be simpler.
The goal is to reduce repeated code without making the component structure harder to understand.
Advantages and Disadvantages
Advantages | Disadvantages |
|---|---|
Reduces repeated Razor markup | Requires initial component setup |
Keeps field styling consistent | Component parameters need careful design |
Reuses validation messages | Complex components can become difficult to maintain |
Makes form updates easier | Overusing components can complicate simple forms |
Improves separation of responsibilities | Debugging data binding may require additional effort |
Common Mistakes to Avoid
One common mistake is forgetting the ValueExpression parameter. Without it, the validation system may not correctly identify the property associated with the reusable input.
Another mistake is using a normal HTML input when the component needs to participate in Blazor's validation system. Prefer the appropriate Blazor input component, such as InputText or InputNumber.
Developers also sometimes create a reusable component with too many parameters. A component should have a clear responsibility. A text field should handle text input, while a dropdown should handle selection.
Finally, avoid placing database operations inside input components. Reusable form components should handle presentation, binding, and field-level validation. Business operations should remain in the page's service layer.
Best Practices for Building Reusable Blazor Forms
Keep reusable form components small and focused. A text field should not contain customer-specific business logic because it may be used by several unrelated pages.
Use Value, ValueChanged, and ValueExpression when building components that need to support two-way binding and field validation.
Keep validation rules close to the model or schema that defines the expected data. This makes them easier to maintain and test.
Use meaningful component names and keep shared components in a dedicated folder. This makes it easier for developers to locate and reuse them.
For larger forms, consider using separate components for sections such as customer details, billing information, and delivery information. The parent form can maintain the overall model and submission process.
Also test reusable components with valid input, missing values, incorrect formats, and changes made through two-way binding. A component that looks correct is not necessarily working correctly with the form's validation context.
Troubleshooting Common Problems
Problem | Possible reason | Solution |
|---|---|---|
Input value does not update | Incorrect binding callbacks | Check |
Validation message does not appear | Missing | Pass the expression to the input and validation message |
Form submits with invalid data | Incorrect submit event or missing validator | Use |
Validation summary is empty | No registered validator | Check the validator and model attributes |
Input component is not recognized | Missing namespace | Import |
Dropdown or number input fails | Incorrect component type or binding type | Match the component's value type to the model property |
Summary
Reusable form components make Blazor applications easier to maintain when the same input fields and validation rules appear across multiple pages.
By creating a shared text field component with Value, ValueChanged, and ValueExpression, developers can reuse the same input markup without losing Blazor's built-in form validation.
Combining these components with EditForm, DataAnnotationsValidator, and model validation attributes provides a clean way to build consistent forms.
Start with a few commonly used fields and introduce additional components when a clear reuse requirement exists. Avoid creating unnecessary abstractions for simple forms.
A well-designed reusable form structure reduces duplicated markup, keeps validation consistent, and makes future changes easier to manage.


Join the conversation! Your thoughts help the community grow.