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:

C#
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:

HTML / XML
<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

C#
<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:

PowerShell
@using System.Linq.Expressions
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Forms

You 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:

C#
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.

TypeScript
@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:

C#
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:

C#
@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 EditForm

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 EditContext validation

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 Value and ValueChanged

Validation message does not appear

Missing ValueExpression

Pass the expression to the input and validation message

Form submits with invalid data

Incorrect submit event or missing validator

Use OnValidSubmit and DataAnnotationsValidator

Validation summary is empty

No registered validator

Check the validator and model attributes

Input component is not recognized

Missing namespace

Import Microsoft.AspNetCore.Components.Forms

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.