Skip to content

Documentation

Forms and validation

EditForm DataAnnotations Core and Extras

The 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-Value names the field, and inside an EditForm the input reads the cascading EditContext, so the form's validator - DataAnnotationsValidator or 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's valid, invalid and modified classes.
  • 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 ErrorMessage and Invalid: 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.

Sign-up form

Submit the form empty: the focus lands on the name, which reads out its message. Fill a field in and its message goes as soon as it passes; submit again and the focus moves on to the first field that is still wrong.

Plan

Four things make it:

  1. The attributes of the model are the rules: [Required], [EmailAddress], [Range], [AllowedValues]. Their ErrorMessage is what the field shows and what a screen reader reads out, so write it for a person.
  2. <DataAnnotationsValidator /> runs the attributes, on every field as it is edited and on the whole form when it is submitted.
  3. ShowValidationMessage on each input shows its messages under it, and <BitFocusOnInvalidSubmit /> takes the user to the first one.
  4. novalidate on the EditForm leaves 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 EditContext for. 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 taken user name

The user names admin, root and bit are taken. Save one of them: the answer of the server shows under the field, and the focus goes back to it. Change the name and the message goes.

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.