NhsWales.BlazorKit.Components
0.2.2
dotnet add package NhsWales.BlazorKit.Components --version 0.2.2
NuGet\Install-Package NhsWales.BlazorKit.Components -Version 0.2.2
<PackageReference Include="NhsWales.BlazorKit.Components" Version="0.2.2" />
<PackageVersion Include="NhsWales.BlazorKit.Components" Version="0.2.2" />
<PackageReference Include="NhsWales.BlazorKit.Components" />
paket add NhsWales.BlazorKit.Components --version 0.2.2
#r "nuget: NhsWales.BlazorKit.Components, 0.2.2"
#:package NhsWales.BlazorKit.Components@0.2.2
#addin nuget:?package=NhsWales.BlazorKit.Components&version=0.2.2
#tool nuget:?package=NhsWales.BlazorKit.Components&version=0.2.2
NhsWales.BlazorKit.Components
Blazor components that render the markup and CSS classes defined by the DHCW component library, so applications get consistent, accessible form controls without hand-writing the HTML or remembering the class names.
This library defines no styling of its own. Every class it emits comes from the component library.
Setup
The stylesheet ships as a static web asset. Reference it from your host page
(App.razor), after any other framework CSS so its rules win:
<link rel="stylesheet" href="@Assets["_content/NhsWales.BlazorKit.Components/css/nhsw.css"]" />
@Assets[...] gives the fingerprinted URL, which MapStaticAssets serves as
immutable. Hard-coding the raw path works but costs a revalidation round-trip on
every page load.
Note the stylesheet sets global html/body typography and background — that is
the design system's intent, not a leak, but it does restyle the whole page.
See NHSW-CSS-VERSION.md for the vendored version and how
to update it.
Then make the components available, typically from _Imports.razor:
@using NhsWales.BlazorKit.Components
NhswTextInput
A single-line text input with a visible label, optional hint text, an optional error state, and the design system's fixed and fluid width variants.
<NhswTextInput Id="nhs-number"
Label="NHS number"
Hint="Enter the 10 digit NHS number"
@bind-Value="Model.NhsNumber"
MaxLength="10"
Width="NhswInputWidth.Width10"
InputMode="NhswInputMode.Numeric" />
renders
<div class="nhsw-form-group">
<label class="nhsw-label" for="nhs-number">NHS number</label>
<div class="nhsw-hint" id="nhs-number-hint">Enter the 10 digit NHS number</div>
<input class="nhsw-input nhsw-input--width-10" id="nhs-number" name="nhs-number"
type="text" aria-describedby="nhs-number-hint" inputmode="numeric" maxlength="10">
</div>
Parameters
| Parameter | Type | Notes |
|---|---|---|
Id |
string |
Required. The input's id, the label's for, and the stem for the hint and error ids. Must not contain whitespace. |
Name |
string? |
The submitted name. Defaults to Id. |
Label |
string |
Required. The visible label. |
Hint |
string? |
Hint text, rendered between the label and the input. |
Value |
string? |
The current value. |
ValueChanged |
EventCallback<string?> |
Enables @bind-Value. |
MaxLength |
int? |
Rendered as maxlength. Values below 1 are ignored. |
Width |
NhswInputWidth |
Width variant. Defaults to Full. |
ErrorMessage |
string? |
When set, renders the component in its invalid state. |
ErrorPrefix |
string? |
Visually hidden prefix on the error. Defaults to "Error:"; empty string omits it. |
Disabled |
bool |
Renders the disabled attribute. |
AutoComplete |
string? |
Rendered as autocomplete. Blank is ignored. |
InputMode |
NhswInputMode |
Rendered as inputmode. Defaults to Default, which omits the attribute. |
Any other attribute is splatted onto the <input>, so data-* hooks,
required, readonly and event handlers such as @onblur work as usual:
<NhswTextInput Id="postcode" Label="Postcode" data-testid="postcode" @onblur="ValidateAsync" />
Attributes the control derives itself are ignored rather than allowed to
overwrite it: id, name, class, aria-describedby, aria-invalid, and the
value/onchange pair the binding owns. Splatting is emitted last, so without
this a stray class would strip the NHS Wales styling — including the invalid
state — and a stray aria-describedby would silently detach the hint and error.
Attributes are omitted entirely when their parameter is unset, matching the library's own markup.
Id and Label are validated at runtime and throw InvalidOperationException
if missing — [EditorRequired] is only a build-time hint and does not fire for a
bound null. This is deliberate: an input with no accessible name is the failure
this library exists to prevent.
Widths
NhswInputWidth maps to the modifier classes in the library's _input.scss.
Fluid widths are a proportion of the container and collapse to full width on
small screens; fixed widths are sized to a number of characters.
| Member | Class | |
|---|---|---|
Full |
nhsw-input--full |
fluid |
ThreeQuarters |
nhsw-input--three-quarters |
fluid |
TwoThirds |
nhsw-input--two-thirds |
fluid |
Half |
nhsw-input--half |
fluid |
Third |
nhsw-input--third |
fluid |
Quarter |
nhsw-input--quarter |
fluid |
Width20 |
nhsw-input--width-20 |
fixed, 20 characters |
Width10 |
nhsw-input--width-10 |
fixed, 10 characters |
Width5 |
nhsw-input--width-5 |
fixed, 5 characters |
Width4 |
nhsw-input--width-4 |
fixed, 4 characters |
Width3 |
nhsw-input--width-3 |
fixed, 3 characters |
Width2 |
nhsw-input--width-2 |
fixed, 2 characters |
Width defaults to Full, so nhsw-input--full is always present. It is width
100%, the same as the base nhsw-input, so the default is visually identical to
the library's plain class="nhsw-input" examples.
Input modes
NhswInputMode controls the inputmode attribute. Default omits it; None,
Text, Decimal, Numeric, Tel, Search, Email and Url render the
matching value.
Errors
<NhswTextInput Id="postcode" Label="Postcode" ErrorMessage="Enter a real postcode" />
<div class="nhsw-form-group nhsw-form-group--error">
<label class="nhsw-label" for="postcode">Postcode</label>
<p class="nhsw-error-message" id="postcode-error">
<span class="nhsw-visually-hidden">Error: </span>Enter a real postcode
</p>
<input class="nhsw-input nhsw-is-invalid nhsw-input--full" id="postcode" name="postcode"
type="text" aria-describedby="postcode-error" aria-invalid="true">
</div>
The form group gains the error modifier, the input gains nhsw-is-invalid and
aria-invalid="true", and the message is referenced by aria-describedby. When
both a hint and an error are present, aria-describedby lists the hint first,
then the error, matching the library.
For Welsh-language pages, set ErrorPrefix="Gwall:" so the screen-reader prefix
is not announced in English.
Accessibility
The component enforces the design system's rules rather than leaving them to the caller:
- Every input has a visible
<label>whoseformatches the input'sid. - Hint and error text are always connected via
aria-describedby. - Invalid fields carry
aria-invalid="true". - There is no placeholder parameter, so placeholder text cannot stand in for a label.
Ids are derived from Id, so each Id must be unique on the page. Repeated
instances in a loop should carry @key.
Value binding
@bind-Value updates the bound property on the input's change event, when the
field loses focus, matching Blazor's built-in InputText.
If the bound property normalises or rejects what the user typed, the field is
updated to show the stored value. A property that strips spaces from an NHS
number will visibly rewrite 123 456 7890 to 1234567890 on blur, and one that
rejects a value outright will restore the previous one.
Who owns the value
Both controls behave the same way here, and which behaviour you get depends on
whether you supply Value — not on whether you supply ValueChanged.
| You supply | Who owns the value | What the user sees |
|---|---|---|
@bind-Value (both) |
You | Your value always wins, so normalising and rejecting both work |
Value only |
You | Your value is restored on the next render, overwriting any edit |
ValueChanged only |
The control | The edit is kept and reported to you |
| Neither | The control | The edit is kept, like a plain HTML control |
The controls are controlled elements, so their next render overwrites whatever
the browser holds. When you supply Value that is the point: a value you reject
does not stay on screen. When you do not, there is nowhere to put the input, so
the control keeps it rather than appearing to ignore the user.
An instance the renderer reuses for a different field drops any input it was
holding, so repeated controls do not misattribute an answer — but give looped
instances a @key anyway.
Scope
This component covers the parameters in the user story. The library's nhswInput
macro also supports prefixes and suffixes, alternative input types, label sizes,
and a live character count. Those are deliberately not implemented yet.
MaxLength renders only maxlength. The library's macro would also add a live
character-count hint, which needs nhsw-behaviours.js; that behaviour is
intentionally left out.
NhswSelect
A dropdown for choosing one option from a list. Generic over the bound value, so
it binds directly to string, int, Guid, an enum, or their nullable forms.
The design system advises using a select only as a last resort in public-facing services — prefer radios for short lists.
<NhswSelect TValue="string"
Id="region"
Label="Choose region"
Hint="Select the region where you are based"
Items="Regions"
@bind-Value="Model.Region"
Width="NhswSelectWidth.Medium" />
private static readonly NhswSelectItem<string>[] Regions =
[
new("", "Choose region"),
new("north-wales", "North Wales"),
new("mid-wales", "Mid Wales"),
new("south-wales", "South Wales")
];
renders
<div class="nhsw-form-group">
<label class="nhsw-label" for="region">Choose region</label>
<div class="nhsw-hint" id="region-hint">Select the region where you are based</div>
<select class="nhsw-select nhsw-select--m" id="region" name="region" aria-describedby="region-hint">
<option value="">Choose region</option>
<option value="north-wales">North Wales</option>
<option value="mid-wales">Mid Wales</option>
<option value="south-wales">South Wales</option>
</select>
</div>
Parameters
| Parameter | Type | Notes |
|---|---|---|
Id |
string |
Required. The select's id, the label's for, and the stem for the hint and error ids. Must not contain whitespace. |
Name |
string? |
The submitted name. Defaults to Id. |
Label |
string |
Required. The visible label. |
Hint |
string? |
Hint text, rendered between the label and the select. |
Value |
TValue? |
The selected value. |
ValueChanged |
EventCallback<TValue?> |
Enables @bind-Value. |
Items |
IEnumerable<NhswSelectItem<TValue>> |
Required. The options, in order. Null renders an empty select. |
Width |
NhswSelectWidth |
Width variant. Defaults to Default. |
ErrorMessage |
string? |
When set, renders the component in its invalid state. |
ErrorPrefix |
string? |
Visually hidden prefix on the error. Defaults to "Error:"; empty string omits it. |
Disabled |
bool |
Disables the whole select. |
Any other attribute is splatted onto the <select>, with the same reserved names
as the text input plus one more: multiple is stripped. This component binds a
single value, and the design system directs multi-select use cases to checkboxes
instead.
NhswSelectItem<TValue>
| Property | Type | Notes |
|---|---|---|
Value |
TValue? |
The value bound when the option is selected. |
Text |
string |
The text shown to the user. |
Disabled |
bool |
When true, the option is rendered disabled and cannot be chosen. |
Construct with new(value, text), optionally new(value, text, disabled: true),
or with an object initialiser.
Generic values and placeholders
An item with a null value renders an option with no value attribute, which
is what the library's macro does for placeholders. Use a nullable TValue so
choosing it binds back as null:
<NhswSelect TValue="int?" Id="year" Label="Year of birth" Items="Years" @bind-Value="Model.Year" />
private static readonly NhswSelectItem<int?>[] Years =
[
new(null, "Choose year"),
new(1980, "1980"),
new(1990, "1990")
];
For string, an empty-string value works equally well as a placeholder, as in the
first example.
The selected value is resolved by matching what the browser submits back against
the items you supplied, rather than by parsing it into TValue. That keeps any
value type working, including ones no converter handles.
When no option matches the bound value — including when nothing is bound — no
option is marked selected and the select carries no value, so the browser shows
the first option. That is why a placeholder should come first in the list: for a
non-nullable TValue such as a plain int or an enum, the default value usually
matches no option, and the browser will display the first one while the bound
property still holds the default.
Every option must be distinguishable by the string it submits. A TValue with no
meaningful string form — a plain class, say — formats every option identically,
so the component throws rather than silently resolving every selection to the
first item. Use a record, an id, or an enum.
Widths
NhswSelectWidth maps to the modifier classes in the library's _select.scss.
Each sets a min-width; the select still grows to fit its longest option, up to
the width of its container.
| Member | Class | Minimum width |
|---|---|---|
Default |
(none) | 12rem, from the base nhsw-select rule |
Full |
nhsw-select--full |
fills the container |
ExtraSmall |
nhsw-select--xs |
4rem |
Small |
nhsw-select--s |
8rem |
Medium |
nhsw-select--m |
12rem |
Large |
nhsw-select--l |
16rem |
ExtraLarge |
nhsw-select--xl |
24rem |
Default and Medium render the same width; they differ only in whether the
modifier class is emitted.
Errors
Setting ErrorMessage produces the same invalid state as the text input: the
error modifier on the form group, nhsw-is-invalid and aria-invalid="true" on
the select, and the message referenced by aria-describedby. When a hint is also
present, the hint is listed first.
Accessibility
As with the text input, the component enforces the design system's rules: a
visible label whose for matches the select's id, hint and error text always
connected via aria-describedby, and aria-invalid on invalid fields. Id and
Label are required and validated at runtime.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.AspNetCore.Components.Web (>= 10.0.11)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.