Feature Specification: Large Forms

Created: 2026-09-22 Status: Implemented Input: Reconstructed from the code at d225ebe (src/collapse.ts, src/collapse-renderer.ts, src/lazy-render.ts, src/lazy-object-renderer.ts, patches/jsonforms-vue-stable-control.nix, src/index.ts: the collapsible, progressive, settled, expandAll and collapseAll parts; their unit and DOM tests), docs/usage.md, the pull requests of fediversity/nix-module-form about the raw schema, batch render and collapsed sections, and the Claude Code logs of the maintainers.

User Scenarios & Testing (mandatory)

User Story 1 - Find the relevant options in a large module (Priority: P1)

A module has hundreds of options in nested submodules. The form shows each object section behind a header that the user can open. The sections start closed, except the sections that hold something the user must see: a difference from the baseline, a schema change, or a field that failed validation.

Why this priority: in one flat column of hundreds of controls, the user cannot find the option they came for.

Independent Test: the collapse DOM tests mount a nested schema, and check which sections are open.

Acceptance Scenarios:

  1. Given a nested schema and a value equal to the baseline, When the form mounts, Then each section is closed and its header shows the number of options.
  2. Given a field that differs from the baseline, When the form mounts, Then the section of the field and each section above it are open.
  3. Given a failed validation, When the errors show, Then each section that holds a failing field opens.
  4. Given a closed section, When the user clicks its header, Then it opens.
  5. Given a mounted form, When the user or the embedder uses "Expand all" or "Collapse all", Then all sections open or close, also sections that mount later.

User Story 2 - The form paints quickly (Priority: P2)

By default, the top of the form shows at once, and the remaining sections fill in over the next frames.

Why this priority: a large schema blocks the main thread for about one second when the form renders all at once.

Independent Test: mount a large schema with the default options, check that placeholders show first, and wait for settled.

Acceptance Scenarios:

  1. Given the default options, When the form mounts, Then the sections show placeholders and fill in over the next frames.
  2. Given the default options, When the embedder awaits settled, Then all sections are rendered.
  3. Given progressive: false, When the mount call returns, Then the whole form is in the DOM, and settled resolves on the next frame.

User Story 3 - Edits stay fast (Priority: P2)

The user types in a field or turns a switch in a form with hundreds of options. The form shows the change without a delay.

Why this priority: without a fix, each edit makes every component of the form render again, and the cost increases as the square of the form size.

Independent Test: the stable-control DOM test mounts a form, makes one edit, and counts the components that render again.

Acceptance Scenarios:

  1. Given a mounted form, When the user edits one field, Then only a small number of components render again, not the whole form.
  2. Given an edit, When the new props of a control are equal to its previous props, Then the control keeps its previous props object and does not render again.

Edge Cases

  • A closed section keeps its body in the DOM (hidden). Renderers inside it set their initial data at mount, so the value does not depend on which sections the user opened, and a script can reach each control.
  • The embedder sets collapsible: false: the form renders as one flat column, with no headers and no toolbar. expandAll and collapseAll do nothing.
  • The user switches the branch of a tagged union after "Expand all": the new branch opens too.
  • Sections are objects, maps, lists of objects, and nullable or tagged-union objects. Scalar fields are never in a section of their own.

Requirements (mandatory)

Functional Requirements

  • FR-001: By default, the widget MUST render each object section behind a header that opens and closes it. The option collapsible: false MUST turn this off.
  • FR-002: A section MUST start open when it or a section below it holds a difference from the baseline, a schema change, or (after a failed validation) an error. Other sections MUST start closed.
  • FR-003: A closed section MUST keep its body in the DOM.
  • FR-004: A section header MUST show counts, for example the number of options, entries or changed fields.
  • FR-005: The widget MUST show a toolbar with "Expand all" and "Collapse all", and the handle MUST offer expandAll() and collapseAll().
  • FR-006: By default, the widget MUST render sections in batches over animation frames, with a budget of leaf controls for each frame that the embedder can set. The option progressive: false MUST turn this off, so that the whole form is in the DOM when the mount call returns.
  • FR-007: The handle MUST offer a settled promise that resolves when all sections are rendered.
  • FR-008: The widget MUST give the schema to the renderers as a plain object, not as a reactive proxy, so that large schemas do not slow down each lookup.
  • FR-009: An edit MUST render again only the components whose props change. The build patches @jsonforms/vue so that the control computed of useControl and the renderer computed of useJsonFormsRenderer return the previous object when the new object is equal, and so that DispatchRenderer renders its child without slots when it has no slots. The compare MUST NOT find two values equal that are not deep-equal: data, schema and rootSchema by identity, uischema structurally to depth 8, other keys structurally to depth 3, and only arrays and plain objects structurally.

Key Entities

  • Section: an object, map, list of objects, or nullable or tagged-union object, with a header and a body.
  • Collapse controller: the shared open and closed state of the sections of one form, and the last "Expand all" or "Collapse all" command.
  • Reveal scheduler: the queue that renders sections in batches in progressive mode.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: In a form without differences, the user sees only the top-level sections at mount.
  • SC-002: Each difference, schema change and validation error is visible without a click.
  • SC-003: With progressive rendering, the first paint of a schema with several hundred options takes less than 100 ms on the hardware of the maintainers.
  • SC-004: The value that the widget reports is the same with collapsible on and off.
  • SC-005: On a schema with 159 leaves, on the hardware of the maintainers, an edit takes approximately 55 ms and the mount takes approximately 1.5 s (without the @jsonforms/vue patch: 310 ms and 4.4 s).

Assumptions

  • The embedder that captures images of the page or drives it from a script awaits settled first.

Known Deviations

None.