GrFormField
Combines label, control, hint and error into a single field.
Overview
When to take it
- the field has a label — it is linked to the control by
idautomatically, with no manualfor; - the field shows an error — the text, the red border and
aria-invalidreach the control through the context; - the field takes part in the validation of a form —
nameconnects it toGrForm; - the label stands to the side —
labelPositionandlabelWidthgive a horizontal layout without a grid of your own.
When to take something else
| Need | Take |
|---|---|
| Rules and the blocking of submission | GrForm |
| A control without a label and an error | the control itself: GrInput, GrSelect, … |
| A block of fields with a heading | GrFormSection |
| The error belongs to the whole form rather than to a field | GrResponseErrorBanner |
Playground controls come from the API, not from hand
Loading…
0 changed· state is in the URL
<GrFormField label="Email" hint="We never show it to other members."><GrInput modelValue="[email protected]" /></GrFormField>Examples 3 each with a live demo and its code
Errors from the server, field by field
Without GrForm the server is the only judge: its 422 answer is mapped onto the fields through
error, which takes a string or a list. The message container lives in the DOM all the time and is part of the control’saria-describedby, so a screen reader hears it with the field.requiredputs the marker in the label andaria-requiredon the control.Invite a teammate Settings as a table of pairs
label-position="start"puts the labels in their own column, andlabel-widthkeeps that column the same in every row, so a select, a text field, a number with a unit and a note line up. What a value means is said by the control: the next number and the period sit in its addons.Invoice defaultsApplied to new invoices; existing ones keep their terms. next: INV-041963 / 160A radio group with markup in the label
A radio group is a role, not an input, so the field names it through
aria-labelledby, andrequiredreaches every radio. Thelabelslot carries a badge and thehintslot a link — markup thelabelandhintprops cannot hold.Times are for Dublin. Shipping policy
Standard 2–3 working days · €4.90Express Tomorrow before 13:00 · €9.90Pick up in store Grafton Street, from today 17:00 · free
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
disabled | boolean | undefined | false | The field is unavailable. It adds to the `disabled` of the form by "or". |
readonly | boolean | undefined | false | The whole field is read-only: the controls inside stop being edited. |
required | boolean | undefined | false | Marks the field as required: a `*` marker in the label and `aria-required` on the control. The marker is `aria-hidden`, so it is the control that declares the requirement — and how exactly is decided by its role. The breakdown of the `role="group"` case, which `aria-required` does not allow, is on the page of the component. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size of the label, the hint and the error. Unset — from `GrConfigProvider`, otherwise `md`. |
name | string | undefined | undefined | The name of the field in the model of `GrForm` (a dot path such as `address.city` included). When the field is inside a `GrForm` and `name` is set, the error and the requiredness are taken from the form by that name, and the loss of focus triggers the validation of the field. |
label | string | undefined | undefined | The label of the field: the `<label for>` of the control and its accessible name. The `#label` slot is stronger. |
labelPosition | "top" | "start" | undefined | undefined | The label above (the default) or to the side — for dense forms. |
error | string | string[] | undefined | undefined | An explicit error (or several). It overrides the error from `GrForm` — the manual mode without a form is here as well. The array is needed where the source of the errors is one and the complaints are several: the answer of the server, the validation of a file. |
labelWidth | string | number | undefined | undefined | The width of the column of the label with `labelPosition="start"`. A number is pixels. |
reserveMessage | boolean | undefined | undefined | Hold space for the message line while there is no error: the appearing text does not shift the form, a dialog or a fixed-height panel. Unset — taken from `GrForm` (`reserveMessage`), then from `GrConfigProvider`. Several errors (`error: string[]`) still grow below a single line. |
forId | string | undefined | undefined | An explicit id of the control. If it is not set, one is generated automatically. |
hint | string | undefined | undefined | A hint under the label or above the control (also available through the `#hint` slot). |
showMessage | boolean | undefined | true | Whether to show the text of the error. `false` — the field stays invalid for the control and for AT (`aria-invalid`), but the message takes up no room: that is done in dense table-like forms, where the error is explained by a summary above. |
labelClass | LabelClass | undefined | undefined | Classes on the `<label>` on top of the built-in ones — a targeted tweak of the label without a slot of your own. |
Slots
| Slot | Type | Description |
|---|---|---|
default | any | The control of the field. |
label | any | The label instead of the `label` prop. |
hint | any | The hint under the control instead of the `hint` prop. |
error | { errors: string[]; } | The text of the error instead of the standard one. It receives the already resolved list. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrFormFieldContext, GrFormFieldConfigurableProps, GrFormFieldSize } from '@feugene/granularity/components/GrFormField'Filedist/types/components/shared/formFieldContext.d.ts
interface GrFormFieldContext {
id: ComputedRef<string | undefined>;
labelId: ComputedRef<string | undefined>;
describedById: ComputedRef<string | undefined>;
invalid: ComputedRef<boolean>;
required: ComputedRef<boolean>;
readonly: ComputedRef<boolean>;
disabled: ComputedRef<boolean>;
}function useGrFormFieldContext(): GrFormFieldContext | null;Filedist/types/components/GrFormField/config.d.ts
const grFormFieldConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFormField">;Filedist/types/components/GrFormField/defaults.d.ts
interface GrFormFieldConfigurableProps {
size: GrFormFieldSize;
labelPosition: GrFormFieldLabelPosition;
reserveMessage: boolean;
}Filedist/types/components/GrFormField/grFormFieldStyles.d.ts
type GrFormFieldSize = GrComponentSize;type GrFormFieldLabelPosition = typeof GR_FORM_FIELD_LABEL_POSITIONS[number];Filedist/types/components/GrFormField/safelist.d.ts
const grFormFieldSafelist: string[];Filedist/types/components/GrFormField/GrFormField.vue.d.ts
interface GrFormFieldProps {
label?: string;
name?: string;
forId?: string;
error?: string | string[];
hint?: string;
required?: boolean;
readonly?: boolean;
disabled?: boolean;
showMessage?: boolean;
size?: GrFormFieldSize;
labelPosition?: GrFormFieldLabelPosition;
labelWidth?: string | number;
labelClass?: LabelClass;
reserveMessage?: boolean;
}Usage from the library prose
The error is declared rather than appearing
The container of the error always lives in the DOM and changes only its text, and the
aria-describedby of the control always contains its id. The reason: some AT do not re-read
the description after the attribute itself has changed — an error added together with a new
aria-describedby could stay unread. An empty container goes into sr-only, so no empty
line appears in the markup of the field.
error accepts both a string and an array: one field sometimes has several complaints (the
server’s answer, the validation of a file). Inside a form the error is taken from GrForm
by name, and an explicit prop overrides it.
showMessage: false leaves the field invalid for the control and for AT but does not show
the text — for dense forms where the errors are explained by a summary above.
Validation on blur — only on a real departure
focusout also bubbles when the focus moves inside the field: from an input to its own
clear button, between the checkboxes of a group. The field is validated only if the focus
has left the bounds of the root (relatedTarget outside the field or null) — otherwise it
turned red before the user had finished filling it in.
Linking the label to the control
<label for> points at the id from the context, and it is the control itself that hangs
that id on itself (useGrFormFieldContext()). If there is no such element inside the
field, in dev mode the component warns in the console: otherwise a click on the label
silently does nothing, and for a screen reader there is no link at all.
Widgets with an ARIA role (GrCheckbox, GrRadioGroup, GrCheckboxGroup) do not support
<label for> — they take their name through aria-labelledby on field.labelId.
Custom references are added to the field's own
aria-describedby, aria-labelledby and aria-errormessage set on the control go to the
element with the role or to the input itself — not to the wrapper, where they mean nothing —
and are combined with the id of the field’s hint and error, separated by spaces, without
duplicates. So an explanatory line under a checkbox outside GrFormField is announced, and
inside a field it is announced together with the field’s hint:
<template>
<GrCheckbox v-model="consent" aria-describedby="consent-terms-note">
I agree
</GrCheckbox>
<p id="consent-terms-note">
The terms can be withdrawn in your profile.
</p>
</template>Requiredness is declared by the control, and the role decides how
The * marker in the label is marked aria-hidden: for a screen reader it does not exist at
all, and the only source of requiredness is the control itself. It takes required from the
context through useGrFormFieldContext() and turns it into aria-required.
The direct path is not open to every role. aria-required and aria-readonly are allowed by
textbox, radiogroup, checkbox, combobox and their neighbours, but not by
role="group": axe fails that as a critical aria-allowed-attr. A group control therefore
declares both states on its focusable descendants rather than on itself. A working
example is GrCheckboxGroup: the required of the group reaches the
aria-required of every checkbox.
aria-invalid, on the contrary, stays on the group itself. Duplicated on every item, it
would make the narrator repeat “invalid entry” as many times as there are checkboxes in the
group; on an error the items only change colour.
That concerns your widget too: a role with a restricted set of attributes is not a reason to leave a field without a declared requiredness, but a reason to push it down to the descendants.
The layout
labelPosition="start" puts the label on the left, and labelWidth sets the width of its
column — otherwise the columns of controls in a form drift apart by the length of the
labels. The hint and the error stay beside the control: they are about it, not about the
label. With the label to the side both stand below the control, and the label is as
tall as a single-line control of its size and centred, that is, on the line of the text in
the field. Above the control the hint would pull the label away from the field: rows with a
hint and without one would drift apart. The order of the descriptions for AT is set by
aria-describedby (the hint, then the error), not by the DOM.
size (xs…lg) scales the label, the hint, the error and the vertical rhythm; it is taken
from GrConfigProvider unless it is set locally.
No layout shifts
The error is rendered in the flow below the control: a submit with three empty fields adds
three lines, and a form in a dialog, a sliding panel or a fixed-height block jumps or gets
clipped. reserveMessage holds space for the message line while there is no error — the
appearance of the text does not change the height of the field:
<template>
<GrFormField label="IBAN" :error="ibanError" reserve-message>
<GrInput v-model="iban" />
</GrFormField>
</template>Not set on the field — it is taken from GrForm (reserveMessage for the whole form), then
from GrConfigProvider (componentDefaults.GrFormField.reserveMessage). The field’s own
prop is stronger: :reserve-message="false" removes the reserve from one field in a
reserving form. One line is reserved: several errors (error: string[]) and a long text
that has wrapped onto a second line still grow below it. showMessage: false holds no
reserve — there will be no text.
The slots
#label, #hint, #error (which receives errors: string[]) and the default one — the
control itself.
What is missing
validateStatus (validating/success): GrForm has no channel for the state of an
asynchronous rule, and the prop would be purely manual — the status is needed on the side of
the form first.
Props in this example