Skip to content

docs(FormField): document the four remaining slots, and what #error actually does - #496

Merged
IgorShevchik merged 4 commits into
mainfrom
docs/form-field-slots
Aug 26, 2026
Merged

docs(FormField): document the four remaining slots, and what #error actually does#496
IgorShevchik merged 4 commits into
mainfrom
docs/form-field-slots

Conversation

@IgorShevchik

@IgorShevchik IgorShevchik commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Linked issue

Resolves #462 · uncovered #497

Type of change

  • Documentation (updates to the documentation or readme)
  • Bug fix (a non-breaking change that fixes an issue)
  • Enhancement (improving an existing functionality)
  • New feature (a non-breaking change that adds functionality)
  • Chore (updates to the build process or auxiliary tools and libraries)
  • Revert (undoing a merged change — retitle this PR revert(Scope): ...)
  • Breaking change (fix or feature that would cause existing functionality to change)

src/ is untouched. Nothing is fixed here — a real defect is documented and pinned, and filed as #497.

Description

#461 documented #label. The other four slots had no examples, so the only signal they exist was the generated API table — which nobody reads to discover a capability.

Writing the examples turned up two behaviours, and the review round turned up that I had only half-understood the second one.

A hint needs a label

It renders inside the label row, and the component only draws that row when label or #label is present. So hint on a label-less field renders nothing, silently. That is also why with hint and with hint slot were byte-identical to with b24ui in the snapshots — two more of the vacuous cases #454 was about, sitting in its baseline. Both now pass a label; the baseline shrinks from 293 groups to 291.

The blocks and the announcement disagree

FormField decides what to render from props.x || !!slots.x, and what to announce from the props alone:

// useFormField.ts:127
const descriptiveAttrs = ['error', 'hint', 'description', 'help']
  .filter(type => formField?.value?.[type])   // ← props, via provide(). No slots.

Measured, across all five blocks:

what is passed block on screen named in aria-describedby
description prop yes yes
#description slot, no prop yes no
#hint slot, no prop yes no
#help slot, no prop yes no
hint prop, no label no yes

The first version of this PR taught the broken shape. Every example used the slot without the prop, which is exactly the row where the announcement disappears — and three statements on the page were false as written: that the hint is announced through aria-describedby, that the description association "is unchanged", and that #help has no trap of its own. All three are gone.

The page now leads with the rule that fixes all five — set the prop, use the slot for markup — which is the shape #label has had since #461.

#error keeps its own section, because its v-if is satisfied by the slot's mere existence:

v-if="props.error !== false && ((typeof error === 'string' && error) || !!slots.error)"
v-else-if="props.help || !!slots.help"

An #error slot with no error value: a red error block on screen, help gone, aria-invalid="false", and aria-describedby naming a …-help that is no longer in the document. The recommended pattern — bind error to the message or to false — is spelled out with its three states, and the section admits that even its "good" row still names …-help while the block is not rendered.

Checked against upstream first

Per PORTING.md §7, before writing a word: git -C <mirror> show <cursor>:src/runtime/components/FormField.vue. The component and the composable are line-for-line identical to nuxt/ui. So this is upstream's design, fixing it here is a deliberate divergence that a later port would silently revert, and that decision belongs in its own change rather than in a documentation PR. Filed as #497 with the measurements and three repair options.

Tests

The issue asked for these, and it was right — the existing snapshot cases assert neither the association nor the scoped payload. Review found my first attempt was itself vacuous for the case that mattered.

  • the association surviving the #hint / #description slots with the prop set — the recommended shape;
  • three slot-only cases pinning the missing association — the shape the docs used to teach, which the prop-and-slot test could not see;
  • the dangling …-hint when hint is set without a label;
  • all three states of the #error branch, including the dangling reference;
  • the scoped payload arriving in each slot;
  • binds hints with aria-describedby — pre-existing — gains a label and an assertion that the id resolves. It checked one end of an association, which is how the dangling reference stayed hidden.

Each characterization test points at #497, so when the defect is fixed they fail and mark the documentation that has to change with them.

Also

documentation.md gains the command that checks a page renders, corrected after review to name pnpm dev:prepare and to say that CI runs docs:full:generate (pnpm build && pnpm docs:generate) — the environment is not the only difference. MDC fails at render time, and without the NUXT_PUBLIC_* variables the build dies with ERROR completable is not defined, a @nuxtjs/mcp-toolkit symbol with nothing to do with your page. Confirmed identical on a clean main before writing that down.

skills/b24-ui-nuxt/references/guidelines/forms.md is mirrored and restructured: the #label example had been orphaned under the new prose, v-else said where the component says v-else-if, and description described as "below the input" when it renders above.

Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Gate: lint 0 · typecheck 0 · test 314/314 · test:module 3/3 · test:workflows 49/0 · skill:check clean · baseline regenerates to 291/870 with no drift · docs:generate 1264 routes, form-field among them.

claude added 2 commits August 25, 2026 12:36
…ctually does

#461 documented `#label`. The other four slots had no examples, so the only
signal they exist was the generated API table — which nobody reads to discover
a capability.

`#description` and `#hint` get sections in the shape #461 established. Two
facts came out of writing them that the page could not have told you:

**A hint needs a label.** It renders inside the label row, and the component
only draws that row when `label` or `#label` is present. A `hint` on a
label-less field renders nothing, silently — which is also why `with hint` and
`with hint slot` were byte-identical to `with b24ui` in the snapshots (#454).
Both cases now pass a label, and the collision baseline shrinks by two groups.

**`#error` is worse than "not a drop-in replacement".** The issue said the
error block renders unconditionally and hides `help`; both are true. What it
did not say is that the aria wiring is computed from the props and does not
follow. Measured, with `help` set and an `#error` slot and no error:

  error block   rendered, in red
  help block    gone
  control       aria-invalid="false"
                aria-describedby="v-0-0-help"   <- an id no longer in the DOM

A visible error nobody is told about, over a dangling reference. The page now
shows the pattern that works — bind `error` to the message or to `false`, and
let the slot supply markup only — and says why a Form-driven field cannot use
it, since the Form sets `undefined` rather than `false`.

Checked against upstream at our cursor before writing any of it: the branch is
line-for-line identical to `nuxt/ui`, so this is their design and documenting
it is the right move rather than diverging (PORTING.md §7).

Tests for what the snapshots cannot see: the `aria-describedby` association
surviving the `#hint` and `#description` slots, the scoped payload arriving in
each, and all three states of the `#error` branch — including the dangling
reference, pinned so it cannot be fixed by accident and go unnoticed.

Mirrored into the skill guideline, whose props table also omitted `help` and
`error` entirely.

Also: `documentation.md` gains the command that checks a page renders. MDC
fails at render time, and without the `NUXT_PUBLIC_*` variables `deploy.yml`
sets, the build dies with `ERROR completable is not defined` — a
`@nuxtjs/mcp-toolkit` symbol, nothing to do with your page, and identical on a
clean `main`. That cost me twelve minutes; it should cost the next person none.

Resolves #462
claude added 2 commits August 25, 2026 14:22
…shape

The first version of this PR found that `#error`'s aria wiring is computed from
the props and does not follow the slots. It did not carry that insight to the
other four slots, whose blocks render on the same `props.x || !!slots.x` while
`useFormField` builds `aria-describedby` from `formField.value[type]` — the
props, through `provide`. Slots are not in there at all.

Every new example on the page used the slot **without** the prop. That is the
one shape where the announcement disappears. Measured:

  #hint slot, no prop          block rendered, aria-describedby absent
  #description slot, no prop   block rendered, aria-describedby absent
  #help slot, no prop          block rendered, aria-describedby absent
  hint prop, no label          block NOT rendered, aria-describedby names it

Three statements were false as written and are gone: that the hint is announced
through `aria-describedby` (not in the example above it), that the description
association "is unchanged" (only with the prop set), and that `#help` has no
trap of its own.

The page now leads with the rule that fixes all five — set the prop, use the
slot for markup, which is the shape `#label` has had since #461 — with the
disagreement tabulated in both directions. The `#error` section keeps its
detail, and admits that even the recommended pattern names `…-help` while the
help block is not rendered.

Tests for the shape the docs teach, which the first version could not see: it
set prop *and* slot, so it passed on the prop's strength. Three slot-only cases
now pin the missing association, and one pins the dangling `…-hint`. The
pre-existing `binds hints with aria-describedby` gets a label and an assertion
that the id resolves — it checked one end of an association and was how the
dangling reference stayed hidden.

Filed as #497, with the measurements and the three repair options. Checked
against upstream first: identical at our cursor, so fixing it here is a
divergence to decide on purpose, not a side effect of a docs PR.

Also from review: the skill guideline had the `#label` example orphaned under
the new error prose, said `v-else` where the component says `v-else-if`, and
described `description` as "below the input" when it renders above; the
`documentation.md` recipe implied the environment was the only difference from
CI, which also runs `pnpm build` ahead of it.

Refs #462
@IgorShevchik
IgorShevchik merged commit 4560d54 into main Aug 26, 2026
2 checks passed
@IgorShevchik
IgorShevchik deleted the docs/form-field-slots branch August 26, 2026 04:31
IgorShevchik added a commit that referenced this pull request Sep 8, 2026
… were set (#549)

The template renders each descriptive block from `props.x || !!slots.x`, while
`useFormField` built `aria-describedby` from the props handed down through
`provide`. Slots were not in that context at all, so the two conditions
disagreed in both directions: a slot with no prop was text on screen that
nothing announced, and a prop whose block never drew left the attribute naming
an id that was not in the document — `hint` without a label to live in, `help`
behind a rendered error, and `error: true`, which marks the field invalid
without a message the error block could show.

`FormField.vue` is the only scope where props and slots are both visible, so
the ids are computed there and passed through the injected context as
`describedBy`. The template binds the same `hasLabel` / `hasError` computeds
that feed it, so the two cannot drift apart again. `useFormField` prefers that
list and keeps the props-derived one as a fallback, since
`formFieldInjectionKey` and `FormFieldInjectedOptions` are both published and a
custom control may provide a context without it.

`aria-invalid` is deliberately unchanged. It reports the field's error state,
and the `#error` slot's `v-if` is satisfied by the slot merely existing — so
keying invalidity off the rendered block would mark every field invalid for
anyone who supplies the slot as markup.

The six characterization tests added in #496 to pin this behaviour fail as
intended and are rewritten; a seventh covers the `error: true` direction, which
nothing had. Each of the four conditions in `describedBy` was mutated
separately to confirm it is checked.

Upstream still carries the defect at v4 HEAD, so this is a deliberate
divergence: recorded as a §2 invariant in `.sync/PORTING.md`, with the docs
page and the `skills/` forms guideline rewritten to match.

Closes #497

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MsMuj8Fic9tjWVjyEyrxc
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(FormField): the hint, description, help and error slots have no examples

2 participants