Help us improve
Share bugs, ideas, or general feedback.
From dotnet-blazor
Creates Blazor forms with EditForm, input components, DataAnnotationsValidator, FluentValidation, and SSR handling for validation.
npx claudepluginhub markus41/claude --plugin dotnet-blazorHow this skill is triggered — by the user, by Claude, or both
Slash command
/dotnet-blazor:blazor-forms-validationThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
```razor
Explains Blazor component model: render modes (SSR, Interactive Server/WebAssembly/Auto), lifecycle methods, parameters, EventCallbacks, and communication via cascading values. For .NET web app developers.
Building Blazor components. Lifecycle, state management, JS interop, EditForm validation, QuickGrid.
Provides headless, type-safe form state management and validation for React, Vue, Angular, Svelte, Solid, Lit with Zod/Valibot adapters.
Share bugs, ideas, or general feedback.
@rendermode InteractiveServer
<EditForm Model="@_model" OnValidSubmit="HandleSubmit" FormName="create-item">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="mb-3">
<label class="form-label">Name</label>
<InputText @bind-Value="_model.Name" class="form-control" />
<ValidationMessage For="@(() => _model.Name)" />
</div>
<div class="mb-3">
<label class="form-label">Email</label>
<InputText @bind-Value="_model.Email" class="form-control" type="email" />
<ValidationMessage For="@(() => _model.Email)" />
</div>
<div class="mb-3">
<label class="form-label">Category</label>
<InputSelect @bind-Value="_model.CategoryId" class="form-select">
<option value="">Select...</option>
@foreach (var cat in _categories)
{
<option value="@cat.Id">@cat.Name</option>
}
</InputSelect>
</div>
<div class="mb-3">
<label class="form-label">Accept Terms</label>
<InputCheckbox @bind-Value="_model.AcceptTerms" />
</div>
<button type="submit" class="btn btn-primary" disabled="@_submitting">
@(_submitting ? "Saving..." : "Submit")
</button>
</EditForm>
public sealed class CreateItemModel
{
[Required(ErrorMessage = "Name is required")]
[StringLength(200, MinimumLength = 2)]
public string Name { get; set; } = "";
[Required, EmailAddress]
public string Email { get; set; } = "";
[Range(1, int.MaxValue, ErrorMessage = "Select a category")]
public int CategoryId { get; set; }
[Range(typeof(bool), "true", "true", ErrorMessage = "Must accept terms")]
public bool AcceptTerms { get; set; }
}
// Install: Blazored.FluentValidation
public sealed class CreateItemValidator : AbstractValidator<CreateItemModel>
{
public CreateItemValidator()
{
RuleFor(x => x.Name)
.NotEmpty().WithMessage("Name is required")
.MaximumLength(200);
RuleFor(x => x.Email)
.NotEmpty().EmailAddress();
RuleFor(x => x.CategoryId)
.GreaterThan(0).WithMessage("Select a category");
}
}
@using Blazored.FluentValidation
<EditForm Model="@_model" OnValidSubmit="HandleSubmit">
<FluentValidationValidator />
@* ... inputs ... *@
</EditForm>
For static SSR pages, use [SupplyParameterFromForm]:
@page "/items/create"
<EditForm Model="@Model" OnValidSubmit="HandleSubmit" FormName="create-item" method="post">
<AntiforgeryToken />
<DataAnnotationsValidator />
@* inputs *@
</EditForm>
@code {
[SupplyParameterFromForm]
private CreateItemModel Model { get; set; } = new();
private async Task HandleSubmit()
{
await ItemService.CreateAsync(Model);
Navigation.NavigateTo("/items");
}
}
| Component | Binds to | HTML |
|---|---|---|
InputText | string | <input type="text"> |
InputTextArea | string | <textarea> |
InputNumber<T> | int, decimal, etc. | <input type="number"> |
InputDate<T> | DateTime, DateOnly | <input type="date"> |
InputCheckbox | bool | <input type="checkbox"> |
InputSelect<T> | enum, int, string | <select> |
InputRadio<T> | enum, string | <input type="radio"> |
InputFile | IBrowserFile | <input type="file"> |
<InputFile OnChange="HandleFileSelected" accept=".pdf,.docx" multiple />
@code {
private async Task HandleFileSelected(InputFileChangeEventArgs e)
{
foreach (var file in e.GetMultipleFiles(maxAllowedFiles: 5))
{
if (file.Size > 10 * 1024 * 1024) continue; // 10MB limit
using var stream = file.OpenReadStream(maxAllowedSize: 10 * 1024 * 1024);
// Process stream...
}
}
}
@* Two-way binding - updates on element blur by default *@
<input @bind="inputValue" />
<input @bind="InputValue" />
@code {
private string? inputValue;
private string? InputValue { get; set; }
}
@* Update on every keystroke instead of blur *@
<input @bind="searchText" @bind:event="oninput" />
@code {
private string? searchText;
}
@* CORRECT: Use @bind:get/@bind:set for two-way binding with custom logic *@
<input @bind:get="inputValue" @bind:set="OnInput" />
@code {
private string? inputValue;
private void OnInput(string? value)
{
var newValue = value ?? string.Empty;
inputValue = newValue.Length > 4 ? "Long!" : newValue;
}
}
Important: Do NOT use value="@x" @oninput="handler" for two-way binding - Blazor won't sync the value back. Always use @bind:get/@bind:set.
<input @bind="searchText" @bind:after="PerformSearch" />
@code {
private string? searchText;
private async Task PerformSearch()
{
// Runs after searchText is updated
results = await SearchService.SearchAsync(searchText);
}
}
<input @bind="startDate" @bind:format="yyyy-MM-dd" />
@code {
private DateTime startDate = new(2020, 1, 1);
}
@* Parent *@
<YearSelector @bind-Year="selectedYear" />
@code {
private int selectedYear = 2024;
}
@* Child: YearSelector.razor *@
<input @bind:get="Year" @bind:set="YearChanged" />
@code {
[Parameter] public int Year { get; set; }
[Parameter] public EventCallback<int> YearChanged { get; set; }
@* Convention: parameter + "Changed" suffix *@
}
<select @bind="SelectedCities" multiple>
<option value="bal">Baltimore</option>
<option value="la">Los Angeles</option>
<option value="sea">Seattle</option>
</select>
@code {
public string[] SelectedCities { get; set; } = Array.Empty<string>();
}