Building a Form with BPT Controls

~15 min Beginner

Compose a complete data-entry form using BptTextInput, BptDropdown, BptDateSelector, and BptSwitch. Covers two-way binding, validation, and submission.

Home / Learning / Building a Form with BPT Controls
What you'll build A "Sign up for an account" form that captures a name, email address, role, start date, and a newsletter opt-in switch — wired up with two-way binding and DataAnnotations validation. About 60 lines of Razor.

Prerequisites

  • A Blazor project with Blazor Power Tools installed (see Getting Started)
  • <BptRootComponent> wrapped around your layout body
  • @using Bpt.Components.Controls in _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.

Tip — control state vs. component state BPT controls hold no state of their own — they're "controlled" components. The single source of truth is your model field. That means undoing a value is as simple as reassigning the field; the control re-renders to match.

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

ValidatorEffect
TextValidation.NoneNo visual feedback (default).
TextValidation.EmailValidationLive regex check; the input glows red until the value parses as an email.
TextValidation.PasswordValidationRenders 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.Value is DateTime?. If your model is non-nullable DateTime, the compiler will accept the binding but you lose the "no date selected" state — make the field nullable.
  • Dropdown items late. If Items is filled via an async load, the dropdown renders empty until the load completes. Either prefill from a synchronous source or render a BptLoader while waiting.
  • Switch default value. A bool defaults to false — so a "Subscribe to newsletter" switch starts off. Initialize the model field to true if "opted in" is your preferred default.

An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please reload the page.