Documentation
Forms and validation
EditForm DataAnnotations Core and ExtrasThe bit BlazorUI inputs are Blazor form inputs. Inside an EditForm they are validated by the form, through the same EditContext the framework's own inputs use. A few opt-in parameters add what the framework leaves to the app: the message under each field, and the focus on the first invalid field when a submit fails.
At a glance
What an input does inside an EditForm, and the parameter or component behind each part.
- It joins the form.
@bind-Valuenames the field, and inside anEditFormthe input reads the cascadingEditContext, so the form's validator -DataAnnotationsValidatoror any other - validates it with the rest of the form. - It shows the state. A field that fails carries
aria-invalid="true"and the invalid look, and its root takes the framework'svalid,invalidandmodifiedclasses. - It shows the message, with
ShowValidationMessage: the field's messages under it, read out by a screen reader when the field takes the focus. - It takes the focus, with
<BitFocusOnInvalidSubmit />in the form: a failed submit moves the focus to the first invalid field. - It takes errors from the app, with
ErrorMessageandInvalid: a rejection the app knows about, in a form or out of one.
None of it replaces the validation of the framework: the attributes of the model are the rules, the
EditContext holds the messages, and ValidationMessage and
ValidationSummary keep working beside the inputs.
A whole form
Six kinds of input, one model of data annotations, a message under each field and the focus on the first invalid one.
Four things make it:
-
The attributes of the model are the rules:
[Required],[EmailAddress],[Range],[AllowedValues]. TheirErrorMessageis what the field shows and what a screen reader reads out, so write it for a person. <DataAnnotationsValidator />runs the attributes, on every field as it is edited and on the whole form when it is submitted.ShowValidationMessageon each input shows its messages under it, and<BitFocusOnInvalidSubmit />takes the user to the first one.novalidateon theEditFormleaves the checking to the form - the next section says why.
The browser's own check
Why every form on this page carries novalidate.
Required on an input marks it: an asterisk by its label, and the required or
aria-required attribute, so a screen reader announces it as a required field. It validates
nothing by itself - the [Required] of the model does. A browser, though, checks a
required control of its own accord before it lets a form submit: it stops the submit, shows a
bubble of its own in the language of the browser, and the EditContext is never asked. No message
shows under the field, and none of the rules of the model runs until the browser is satisfied.
novalidate turns that check off and keeps the attributes, so the form is validated once, by the
rules of the model, with every message where the app put it. Keep Required on the inputs whose
property is [Required]: it is how the user knows before submitting.
<EditForm Model="model" OnValidSubmit="Save" novalidate>Showing the messages
ShowValidationMessage puts the messages of a field under it; the components of the framework still work beside it.
With ShowValidationMessage, an input shows the messages the EditContext holds for
its field under it, in the place its ErrorMessage goes, and references them through
aria-describedby, so a screen reader reads them out whenever the field takes the focus. They are
not announced as they appear: a submit validates every field at once, and a message from each of them read
out in a row would bury the form under its own errors. The field that takes the focus says what is wrong
with it.
It is off by default, so a form that places its own ValidationMessage components does not show
every message twice. To turn it on for a whole form, cascade it with BitParams. What a
BitParams carries is a default, never an override: a field that sets
ShowValidationMessage itself keeps its own value.
<BitParams Parameters="formParams">
<EditForm Model="model" OnValidSubmit="Save" novalidate>
...
</EditForm>
</BitParams>
@code {
private readonly IBitComponentParams[] formParams =
[
new BitTextFieldParams { ShowValidationMessage = true },
new BitNumberFieldParams { ShowValidationMessage = true },
new BitDatePickerParams { ShowValidationMessage = true },
new BitChoiceGroupParams { ShowValidationMessage = true },
new BitCheckboxParams { ShowValidationMessage = true },
new BitDropdownParams<BitDropdownItem<string>, string> { ShowValidationMessage = true },
];
}
A BitDropdownParams reaches only the dropdowns of its own type arguments, so a form with
dropdowns of other item or value types adds one for each pair.
The messages belong to the EditContext, so the components of the framework show them too:
<ValidationSummary /> lists them all above the form, and
<ValidationMessage For="() => model.Email" /> shows one field's anywhere on the page.
An ErrorMessage of the app's own wins the place under the field while it is set.
The focus on a failed submit
BitFocusOnInvalidSubmit takes the user to the first field to fix.
A failed submit otherwise leaves the focus on the submit button, and a keyboard or screen reader user has to
go looking for what to fix. Put <BitFocusOnInvalidSubmit /> anywhere inside the
EditForm: it renders nothing visible, and once the validation of a submit is over it moves the
focus to the first field of that form, in document order, that carries aria-invalid="true".
- Every input with a message slot carries it when its field fails, and so do the inputs of the framework. A group hands the focus to the item the Tab key lands on: a ChoiceGroup to its checked radio, or its first one.
-
Only a validation of the whole form moves the focus: a submit, or one the app asks the
EditContextfor. A field validated as the user edits it never does. - A validator that finishes asynchronously (.NET 11) is waited for, and the focus is left alone if the user starts editing a field before it finishes.
- It moves the focus in the browser, so it takes an interactive render mode.
BitFocusOnInvalidSubmit has the rest.
Errors only the server knows
A message added to the EditContext after the submit, shown and focused like the others.
A data annotation can only check what the browser has: whether a user name is taken is for the server to
say, after the submit. The way of the framework is a ValidationMessageStore of the app's own on
the EditContext of the form. Add the message of the server to it and call
NotifyValidationStateChanged, and the field shows it, carries aria-invalid, and is
listed by a ValidationSummary, as it would for a message of the model.
Two handlers keep the message true: it is about the value the server was sent, so editing the field
(OnFieldChanged) or submitting again (OnValidationRequested) clears it.
BitFocusOnInvalidSubmit does not move the focus here: the validation of the submit passed, and
the message came after it. Call FocusAsync on the field yourself, after the notification, so
the field already describes itself with the message when the focus arrives.
For a rejection outside any form, or one the app keeps in its own state, set ErrorMessage on the
input: it marks the field invalid and shows the text in the same place, and unlike a message of the
EditContext it is announced by the live region of the field as it appears.
Invalid marks the field without a text, for a reason explained elsewhere on the page. The
TextField page shows both.
Styling the state
The field classes of the framework, on the root of every input.
Inside an EditForm, the root of each input carries the classes the EditContext gives
its field, the same ones InputText carries: valid or invalid, with
modified once the field has changed. They sit on the root, around the label and the messages,
rather than on the native control. The inputs draw their own invalid look without them (the
bit-inv class), so they are there for a stylesheet of the app - the one that already styles the
inputs of the framework:
/* the root of a field the user has changed, once it passes */
.modified.valid {
border-inline-start: 3px solid var(--bit-clr-suc);
padding-inline-start: 0.5rem;
}
An app whose stylesheet expects other names, such as the is-valid and is-invalid of
Bootstrap, sets a FieldCssClassProvider on the EditContext, and the inputs carry
its names instead:
editContext.SetFieldCssClassProvider(new BootstrapFieldCssClassProvider());
public class BootstrapFieldCssClassProvider : FieldCssClassProvider
{
public override string GetFieldCssClass(EditContext editContext, in FieldIdentifier fieldIdentifier)
{
var isValid = editContext.IsValid(fieldIdentifier);
if (editContext.IsModified(fieldIdentifier)) return isValid ? "is-valid" : "is-invalid";
return isValid ? "" : "is-invalid";
}
}
An input with NoValidate carries none of them: it stays out of the EditContext
altogether.
Values that do not convert
Text an input cannot turn into the bound type.
Some inputs read typed text into the bound type: a number into a NumberField, a date into a DatePicker that
takes typing, a color into a ColorPicker. Text they cannot read is not written to the model. The field gets
a message in the EditContext instead, such as "The Age field is not valid.", so the form cannot
be submitted with it. The message names the field by its DisplayName, or by the name of its
property when it has none. The NumberField takes a ParsingErrorMessage of your own, and the
date and time pickers an InvalidErrorMessage.
Which inputs
Every input joins the EditContext; most also have a place for its messages.
These have a message slot under the field, and with it ErrorMessage, Invalid and
ShowValidationMessage:
Checkbox,
ChoiceGroup,
ColorPicker,
DatePicker,
DateRangePicker,
Dropdown,
FileInput,
NumberField,
Rating,
SearchBox,
Slider,
TextField,
TimePicker,
Toggle,
and in Bit.BlazorUI.Extras
CountryPicker,
PhoneInput,
RichTextEditor and
TimeZonePicker.
These join the EditContext the same way, but have no message slot of their own - put a
ValidationMessage beside them:
Calendar,
CircularTimePicker,
FileUpload,
OtpInput,
TagsInput,
and in Bit.BlazorUI.Extras MarkdownEditor.
Feedback
Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.