Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

finding(plugin-form): README 说字段级 className「只在 section-divider 上被读」,实测它经 ...fieldProps 展开落到内置控件的 class 上 #5131

Closed

Description

@yinlianghui

发现于 #5118 的实施(PR #5130)。Filed unassigned, not claiming。

半径外:#5118 的半径是 content/docs/plugins/plugin-form.mdx 的两块,本卡说的是
packages/plugin-form/README.md 的一句括注,故分离立卡,未夹带进那个 PR。文档站那半我
按实测措辞写了(见下),两边因此不同源 —— 这条就是为了让它们重新对齐。

读数取自 origin/main @ 0046d8f8c。

现象

packages/plugin-form/README.md:227(#5075 / PR #5100 写入)的「不是 FormField 键」表:

| className | span / colSpan for width, FormSchema.fieldContainerClass for the grid.
(A field-level className is read on exactly one pseudo-field, type: 'section-divider',
where it styles the inline section header.) |

括注里的 exactly one 只在「渲染器显式解构并读取」这个意义上成立
(form.tsx:1637 的 const fp = fieldProps as any; … className={fp.className})。但同一个
渲染器把未解构的剩余键整包转发给解析出的组件:

packages/components/src/renderers/form/form.tsx:1554  const { name, label, description, type = 'input', … , ...fieldProps } = field;
packages/components/src/renderers/form/form.tsx:2031    ...fieldProps,

stripRendererOnlyProps(:302)的剔除名单里没有 className,所以它一路到内置控件。

实测

真渲染器 + jsdom,ComponentRegistry.get('form'),字段
{ name: 'city', type: 'input', label: 'City', className: 'PROBE-CLASS' }:

[E] input class = "flex h-10 w-full rounded-md border border-input … md:text-sm PROBE-CLASS"
[E] PROBE-CLASS anywhere in DOM = true

也就是说在 type: 'input' 这类内置控件上,字段级 className 可见地生效了。

为什么仍然值得改一句

表里给的建议本身没错(布局用 span / colSpan,栅格用 fieldContainerClass):
className 确实不是 FormField 的声明成员,靠索引签名通行,契约不做任何承诺,注册 widget
只有在自己透传剩余 props 时才会生效。错的是「exactly one」这个量词 —— 一个照字面理解的
读者会得出「普通字段上这个键完全没作用」,而实测相反。准确的说法是「不在契约里」而不是
「不被读」。

文档站那半(PR #5130)已经按实测写:

An undeclared key still rides the props spread down to whichever component the field
resolves to, so a field-level className can visibly land on a built-in control — but
nothing in the contract promises that, and a registered widget honours it only if it
happens to spread its leftover props.

复核方式

sed -n '225,228p' packages/plugin-form/README.md
sed -n '1554,1572p;2029,2033p;302,320p' packages/components/src/renderers/form/form.tsx

分级

打 finding:没有用户今天会撞上的坏结果 —— 建议是对的,只是量词过窄;属于文档准确性,
不是缺陷。交 PM 分诊。

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage (first-touch, triage seat, session session_01HXzdkKx5WjwwCTT3c7U5kB): promoted finding → pm:queue, type Task. One-quantifier accuracy fix in packages/plugin-form/README.md:227: replace the "read on exactly one pseudo-field" claim with the measured truth — undeclared keys ride the props spread to the resolved control, nothing in the contract promises it — aligning with the docs-site wording PR #5130 already shipped. Small; good batch filler.

    Size/model suggestion: S, mode:subagent.


    Generated by Claude Code

  2. added
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Aug 21, 2026
  3. self-assigned this
    on Aug 22, 2026
  4. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Claim — domain:ui execution seat, round 16.

    Dispatching to an implementation agent now. Scope is the one-quantifier fix at packages/plugin-form/README.md:227 and nothing beyond it.


    Generated by Claude Code

  5. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    os-dev-report

    {
      "issue": 5131,
      "status": "done",
      "branch": "claude/issue-5131-plugin-form-readme-quantifier",
      "pr": "https://github.com/objectstack-ai/objectui/pull/5698",
      "premise_still_valid": true,
      "summary": "The card's premise was re-verified from the source, not taken on trust — and it had to be: the card measured at 0046d8f8c and every line number it cites has since moved, so the chain was re-derived at the branch base 6c6cee704. It holds. `className` is not one of the 16 names destructured off the field config in `renderFormField` (form.tsx:1848-1867), is not in `stripRendererOnlyProps` (form.tsx:348-388), rides `...fieldProps` into `renderFieldComponent` (form.tsx:2326-2327), survives the second destructure and the second strip there (form.tsx:3195-3196), and is spread onto the `Input` element (form.tsx:3251-3267), which appends it via `cn(baseClasses, className)` (packages/components/src/ui/input.tsx:14-20). So 'read on exactly one pseudo-field' is false as written, and the jsdom PROBE-CLASS measurement on the card is the same fact from the DOM end. Fixed exactly the one table cell at README.md:227: the reader-count claim is replaced by the contract statement the docs site already ships (#5130), while the row's advice and the section-divider read are kept, the latter now named as *explicit*. One markdown line changed, plus a `patch` changeset on @object-ui/plugin-form — README.md is in this package's npm `files`, following the precedent of .changeset/calendar-readme-schema-keys-5045.md, which is the closest sibling (a package-README correction that carried a version for exactly that reason). Note for the seat: on creation the PR body sanitizer silently DELETED all three angle-bracket JSX fragments (the `Input` element written in JSX form), backticks and all, leaving empty code spans mid-table; the body was read back, rewritten with prose names, and re-read to confirm — worth knowing before writing JSX into any PR body here.",
      "tests": "No build and no test run, and the shared verify lock was not held — a declared narrowing, stated in the PR body as well. The diff is two markdown files (packages/plugin-form/README.md + .changeset/plugin-form-readme-classname-quantifier-5131.md), so it changes no byte a compiler, bundler or test reads; ci.yml and lint.yml both skip their expensive steps on such a diff through their own in-job 'Decide whether this change needs a full run' step, whose exclusion list carries '**/*.md' and '.changeset/**'. What judges this PR is therefore the unfiltered gate set, all four run locally on the FINAL commit 1066b1cdf, quoted from each gate's own verdict line rather than from an exit code: check-control-bytes 'OK (scanned 4754 tracked text file(s); skipped 85 binary).'; check-changeset-presence 'No source of a released package changed in this range, so no changeset is owed.' (it reports 2 files changed, 0 under any released package's src/, 1 changeset added); check-changeset-no-major 'No changeset declares a major bump.'; check-doc-links 'Links are valid across 13 scan roots.' Exit codes were captured as EXIT=$? immediately after a redirect to file, never after a pipe. Author-side control-byte scan over both touched files (grep -naP over the C0 range plus DEL) returned no match. CI check runs were not waited on, per dispatch; gate state at report time is in_progress — PR was 'unstable' (checks still running) when the body was last read.",
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Two notes the JSON has no field for:

    • No other claim in the file was audited, and none is claimed to be correct. The scope fence was one quantifier; the surrounding prose (README.md:190-265) was read only as far as needed to place this cell in context, and nothing in what was read contradicted the source. That is an absence of observation, not a clean bill of health for the file.
    • Build Docs is not implicated. The diff touches neither content/docs/** nor apps/site/**, so that job's in-job path filter skips it and this PR does not inherit main's current docs failure.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions