GrFormField

  • core
  • forms
  • API · 14 props

Combines label, control, hint and error into a single field.

import { GrFormField } from '@feugene/granularity/components/GrFormField'

Overview

When to take it

  • the field has a label — it is linked to the control by id automatically, with no manual for;
  • the field shows an error — the text, the red border and aria-invalid reach the control through the context;
  • the field takes part in the validation of a form — name connects it to GrForm;
  • the label stands to the side — labelPosition and labelWidth give a horizontal layout without a grid of your own.

When to take something else

NeedTake
Rules and the blocking of submissionGrForm
A control without a label and an errorthe control itself: GrInput, GrSelect, …
A block of fields with a headingGrFormSection
The error belongs to the whole form rather than to a fieldGrResponseErrorBanner

Playground controls come from the API, not from hand

Stage

Loading…

0 changed· state is in the URL

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’s aria-describedby, so a screen reader hears it with the field. required puts the marker in the label and aria-required on the control.

    Invite a teammate
  • Settings as a table of pairs

    label-position="start" puts the labels in their own column, and label-width keeps 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-0419
    63 / 160
  • A radio group with markup in the label

    A radio group is a role, not an input, so the field names it through aria-labelledby, and required reaches every radio. The label slot carries a badge and the hint slot a link — markup the label and hint props cannot hold.

    Times are for Dublin. Shipping policy

API generated from the package · cannot drift

Props

PropTypedefaultDescription
disabledboolean | undefinedfalseThe field is unavailable. It adds to the `disabled` of the form by "or".
readonlyboolean | undefinedfalseThe whole field is read-only: the controls inside stop being edited.
requiredboolean | undefinedfalseMarks 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" | undefinedundefinedThe size of the label, the hint and the error. Unset — from `GrConfigProvider`, otherwise `md`.
namestring | undefinedundefinedThe 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.
labelstring | undefinedundefinedThe label of the field: the `<label for>` of the control and its accessible name. The `#label` slot is stronger.
labelPosition"top" | "start" | undefinedundefinedThe label above (the default) or to the side — for dense forms.
errorstring | string[] | undefinedundefinedAn 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.
labelWidthstring | number | undefinedundefinedThe width of the column of the label with `labelPosition="start"`. A number is pixels.
reserveMessageboolean | undefinedundefinedHold 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.
forIdstring | undefinedundefinedAn explicit id of the control. If it is not set, one is generated automatically.
hintstring | undefinedundefinedA hint under the label or above the control (also available through the `#hint` slot).
showMessageboolean | undefinedtrueWhether 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.
labelClassLabelClass | undefinedundefinedClasses on the `<label>` on top of the built-in ones — a targeted tweak of the label without a slot of your own.

Slots

SlotTypeDescription
defaultanyThe control of the field.
labelanyThe label instead of the `label` prop.
hintanyThe 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
import type { GrFormFieldContext, GrFormFieldConfigurableProps, GrFormFieldSize } from '@feugene/granularity/components/GrFormField'

Filedist/types/components/shared/formFieldContext.d.ts

GrFormFieldContextinterface
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>;
}
useGrFormFieldContextfunction
function useGrFormFieldContext(): GrFormFieldContext | null;

Filedist/types/components/GrFormField/config.d.ts

grFormFieldConfigconst
const grFormFieldConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrFormField">;

Filedist/types/components/GrFormField/defaults.d.ts

GrFormFieldConfigurablePropsinterface
interface GrFormFieldConfigurableProps {
    size: GrFormFieldSize;
    labelPosition: GrFormFieldLabelPosition;
    reserveMessage: boolean;
}

Filedist/types/components/GrFormField/grFormFieldStyles.d.ts

GrFormFieldSizetype
type GrFormFieldSize = GrComponentSize;
GrFormFieldLabelPositiontype
type GrFormFieldLabelPosition = typeof GR_FORM_FIELD_LABEL_POSITIONS[number];

Filedist/types/components/GrFormField/safelist.d.ts

grFormFieldSafelistconst
const grFormFieldSafelist: string[];

Filedist/types/components/GrFormField/GrFormField.vue.d.ts

GrFormFieldPropsinterface
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:

vue
<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:

vue
<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.