Building a Form with BPT Controls
Compose a complete data-entry form using BptTextInput, BptDropdown, BptDateSelector, and BptSwitch. Covers two-way binding, validation, and submission.
Prerequisites
- A Blazor project with Blazor Power Tools installed (see Getting Started)
<BptRootComponent>wrapped around your layout body@using Bpt.Components.Controlsin_Imports.razor
Step 1: Define the form model
BPT controls bind to plain C# properties — there's no special model base class. Add DataAnnotations
attributes for validation rules. EditForm and Blazor's built-in validator components
will pick them up automatically.
using System.ComponentModel.DataAnnotations;
public class SignUpForm
{
[Required, StringLength(60, MinimumLength = 2)]
public string FullName { get; set; } = "";
[Required, EmailAddress]
public string Email { get; set; } = "";
[Required]
public string Role { get; set; } = "";
[Required]
public DateTime? StartDate { get; set; }
public bool SubscribeToNewsletter { get; set; } = true;
}Step 2: Wire up the controls
Wrap your inputs in an EditForm. The form's Model attribute is what
DataAnnotationsValidator inspects on submit, and OnValidSubmit fires only when
every annotation passes.
BPT controls use the standard Blazor two-way binding pattern. Use @bind-Value for any
control that exposes a Value / ValueChanged parameter pair — which is all of them.
@page "/sign-up"
<EditForm Model="_form" OnValidSubmit="HandleSubmitAsync">
<DataAnnotationsValidator />
<div class="mb-3">
<label class="form-label">Full name</label>
<BptTextInput @bind-Value="_form.FullName" Placeholder="Jane Smith" />
<ValidationMessage For="@(() => _form.FullName)" />
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<BptTextInput @bind-Value="_form.Email"
Placeholder="jane@example.com"
Validation="TextValidation.EmailValidation" />
<ValidationMessage For="@(() => _form.Email)" />
</div>
<div class="mb-3">
<label class="form-label">Role</label>
<BptDropdown @bind-Value="_form.Role"
Items="_roles"
Placeholder="Pick a role…" />
<ValidationMessage For="@(() => _form.Role)" />
</div>
<div class="mb-3">
<label class="form-label">Start date</label>
<BptDateSelector @bind-Value="_form.StartDate"
Min="DateTime.Today" />
<ValidationMessage For="@(() => _form.StartDate)" />
</div>
<div class="mb-3">
<BptSwitch @bind-Value="_form.SubscribeToNewsletter"
Label="Send me product updates" />
</div>
<button type="submit" class="btn btn-primary">Create account</button>
</EditForm>
@code {
private SignUpForm _form = new();
private List<string> _roles = new() { "Developer", "Designer", "Product Manager", "Other" };
private async Task HandleSubmitAsync()
{
// _form is fully populated and validated here.
await Task.Delay(250);
Console.WriteLine($"Signed up: {_form.FullName} <{_form.Email}>");
}
}Step 3: How the binding works
@bind-Value="_form.FullName" expands to two attributes at compile time: it sets
Value="_form.FullName" on the way in, and subscribes to ValueChanged to assign
back on the way out. This is identical to the binding model used by built-in Blazor inputs like
InputText.
If you want a one-way binding (display only) just use Value="…" without
@bind-. If you need to react to changes and still keep the model in sync,
bind the value and hook the supplemental callback (e.g. OnDateSelected) on
the same control.
Step 4: Validation — built-in vs. DataAnnotations
BPT ships a small set of visual validators on individual controls (the
Validation parameter on BptTextInput is the most common):
| Validator | Effect |
|---|---|
TextValidation.None | No visual feedback (default). |
TextValidation.EmailValidation | Live regex check; the input glows red until the value parses as an email. |
TextValidation.PasswordValidation | Renders a paired confirm-password field and validates both match. |
These don't replace DataAnnotationsValidator — they give the user immediate per-field
feedback while typing. EditForm + [Required] still runs at submit time,
producing the messages shown by <ValidationMessage>.
Step 5: Handle submission
OnValidSubmit only fires when every [Required], [EmailAddress], etc.
annotation has passed. If anything fails, OnInvalidSubmit fires instead — useful for
scrolling the user to the first error, logging the attempt, or showing a summary banner.
private async Task HandleSubmitAsync()
{
try
{
await _accountService.CreateAccountAsync(_form);
_navigationManager.NavigateTo("/welcome");
}
catch (DuplicateEmailException)
{
_serverError = "An account with that email already exists.";
}
}
For async work, return Task from the handler — Blazor will re-render before and after
the await, so a "Saving…" button label or spinner is straightforward to wire up with a
_isSaving flag.
Common gotchas
- Nullable mismatch.
BptDateSelector.ValueisDateTime?. If your model is non-nullableDateTime, the compiler will accept the binding but you lose the "no date selected" state — make the field nullable. - Dropdown items late. If
Itemsis filled via an async load, the dropdown renders empty until the load completes. Either prefill from a synchronous source or render aBptLoaderwhile waiting. - Switch default value. A
booldefaults tofalse— so a "Subscribe to newsletter" switch starts off. Initialize the model field totrueif "opted in" is your preferred default.