Field
A component that shows the label, description, and errors of a form field, and a summary of errors after a failed submit.
UiField
A wrapper that groups the label, description, and errors of a form field. It passes id, aria-describedby, aria-invalid, and required to the input inside, such as UiInput.
Shown to other members.
Enter a full email address.
A native element receives the same attributes.
Code
<script setup lang="ts">
import { shallowRef } from 'vue'
import { UiField, UiInput } from '@wing-land/ui'
const name = shallowRef('')
const email = shallowRef('ada@')
</script>
<template>
<div class="demo">
<UiField label="Name" description="Shown to other members." required>
<UiInput v-model="name" autocomplete="name" />
</UiField>
<UiField
label="Email"
required
required-indicator="asterisk"
error="Enter a full email address."
>
<UiInput v-model="email" type="email" autocomplete="email" />
</UiField>
<UiField
v-slot="field"
label="Color"
description="A native element receives the same attributes."
>
<input
:id="field.id"
type="color"
:aria-describedby="field.describedBy"
:aria-invalid="field.invalid || undefined"
/>
</UiField>
</div>
</template>
<style scoped>
.demo {
max-width: 420px;
padding: 1rem 0;
}
</style>Usage
Put only one input inside each UiField. For native elements, pass the default slot props (id, describedBy, invalid, required, disabled) to the element. By default, required fields show the text "Required" next to the label. To use an asterisk instead, set the requiredIndicator prop on UiField, or requiredIndicator in the config to change every field.
Accessibility
The label is a <label> tied to the input. The description and errors are linked with aria-describedby, and the error state with aria-invalid. Errors are shown with an icon and text, not color alone. With group, the inputs are grouped in a <fieldset> with a <legend>.
API
Props
| Prop | Default | Type |
|---|---|---|
labelRequiredLabel. | — | string |
descriptionDescription below the label. | — | string |
errorError message. Multiple messages are listed. Nothing is shown when empty. | — | string | readonly string[] | null |
requiredShows the required indicator and passes | false | boolean |
disabledDisables the input. | false | boolean |
id
| — | string |
groupRenders | false | boolean | 'radiogroup' |
hideLabelHides the label from the screen while keeping it for screen readers. | false | boolean |
requiredIndicatorHow required fields are marked. Defaults to the value in the config ( | — | UiRequiredIndicator |
Slots
| Slot | Slot props |
|---|---|
defaultThe input. Pass the slot props to native elements. | UiFieldSlotProps |
labelLabel content. | any |
descriptionDescription content. | any |
UiFormErrorSummary
A list of errors placed at the top of a form after a failed submit. It takes focus when shown, and each item moves to the related input.
Code
<script setup lang="ts">
import { shallowRef } from 'vue'
import { UiButton, UiField, UiFormErrorSummary, UiInput, useFormErrors } from '@wing-land/ui'
const name = shallowRef('')
const expiresAt = shallowRef('')
const form = useFormErrors({ ids: { expiresAt: 'token-expires' } })
function checkName() {
if (!name.value.trim()) form.set({ ...form.errors.value, name: ['Enter a name.'] })
}
// Stands in for a request that answers with an ASP.NET Core ValidationProblem.
function submit() {
const errors: Record<string, string[]> = {}
if (!name.value.trim()) errors.name = ['Enter a name.']
if (!expiresAt.value) errors.expiresAt = ['Choose when the token expires.']
if (!form.setFromProblem({ status: 400, errors })) form.clear()
}
</script>
<template>
<form class="demo" novalidate @submit.prevent="submit">
<UiFormErrorSummary :errors="form.summary.value" />
<UiField id="name" label="Name" required :error="form.get('name')">
<UiInput v-model="name" @blur="checkName" @input="name.trim() && form.clear('name')" />
</UiField>
<UiField id="token-expires" label="Expires at" required :error="form.get('expiresAt')">
<UiInput v-model="expiresAt" type="date" @input="form.clear('expiresAt')" />
</UiField>
<UiButton type="submit" variant="solid">Create token</UiButton>
</form>
</template>
<style scoped>
.demo {
display: grid;
gap: 24px;
justify-items: start;
max-width: 420px;
padding: 1rem 0;
}
.demo > :deep(.ui-field) {
justify-self: stretch;
margin: 0;
}
.demo > :deep(.ui-form-error-summary) {
margin: 0;
}
</style>Usage
Errors can be managed with useFormErrors(). Pass a response shaped like { errors: { name: ["…"] } }, such as ASP.NET Core's ValidationProblemDetails, to setFromProblem() to fill both the field errors and the summary.
const form = useFormErrors({ ids: { expiresAt: 'token-expires' } })
const response = await fetch('/api/tokens', { method: 'POST', body })
if (!response.ok && !form.setFromProblem(await response.json())) showGeneralError()
// <UiFormErrorSummary :errors="form.summary.value" />
// <UiField id="name" :error="form.get('name')">…</UiField>Each item links to the input whose id matches the field name. When the id differs, map it with ids. Once the user fixes an input, remove its error with clear(name).
Accessibility
Renders a <section> named by its heading. With autofocus, focus moves to the summary each time a new list of errors arrives, so screen reader users also notice that the submit failed.
API
Props
| Prop | Default | Type |
|---|---|---|
errorsRequiredErrors. | — | readonly UiFormError[] |
titleHeading. Defaults to the locale text. | — | string |
headingLevelHeading level. | 2 | 2 | 3 | 4 |
autofocusMoves focus to the summary when it receives a new list of errors. | true | boolean |