Skip to content

forms(qml): a host slot can draw a nested-object member, and the form encodes it member by member - #822

Merged
Yaraslaut merged 1 commit into
masterfrom
feature/forms-object-slot
Sep 25, 2026
Merged

Yaraslaut merged 1 commit into
masterfrom
feature/forms-object-slot

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

What / why

A struct member (IgnitionSpecimen specimen, std::optional<PycnometerDetermination> determination1) has no built-in control: DynamicForm flattens it to one text field and reports it unrepresentable, so a form carrying one never becomes ready. #809 gave a host slot a way to draw a std::vector<Row> member. This PR does the same for a nested object, following #809's approach.

An object member no slot claims behaves exactly as before. tst_DynamicFormNestedAggregate.qml is untouched and green.

Contract

Where Name Meaning
descriptor isObject Object schema with properties that no typed control claims (a Quantity/Choice is not one). kind stays "object".
descriptor objectFields The member descriptors, in x-order. Recursive: an object member carries its own objectFields and a collection member its itemFields. Bounded by maxObjectDepth (4); a $defs type is never described twice on one path, which makes it cycle-safe.
descriptor claimedBySlot A registered slot resolves for this top-level member, so unrepresentable is "" for it and for every object or collection described inside it.
slot (optional) objectValue Binding to the value as {member: cellText | nestedValue} ({} when blank).
slot (optional) setObject(value) Same as setValue(JSON.stringify(value)).

Encoding rules (encodeObjectValue):

  • Each leaf goes through encodeFieldText, the encoder the member would get at top level: same locale normalisation, precision limit and bounds.
  • A nested object is encoded recursively. A collection inside the object reuses the forms(qml): a host slot can draw a std::vector<Row> member, and the form encodes its rows #809 row encoder.
  • A blank optional leaf is omitted. A blank optional object (absent, {}, or every member blank) is also omitted.
  • A blank required leaf or required object gives null, so the form is not ready. The same happens for a value that is not an object and for a cell that does not encode.

Prefill: prefill / prefillFromJson decode a stored object with decodeObjectValue, the inverse of the encoder, member by member through decodeFieldValue. So prefill → submit round-trips.

Schema-side fix included (include/morph/forms/forms.hpp). recurseIntoNestedAggregateIfAny did not look inside std::optional<Sub>, which glaze emits as anyOf: [<Sub>, {type: null}]. As a result, an optional sub-record's members got no x-order, title or required, and none of their FieldMeta (ExtUnits, x-displayDecimals, label). It now recurses into the non-null branch, and that also covers an optional std::vector<Sub>.

Tests

  • New src/qt/forms/tests/tst_DynamicFormObjectSlot.qml, 24 cases, with assertions on the submitted body. It covers:
    • descriptor shape: depth 2, units, decimals and labels from nested FieldMeta, a self-referential type stopping, and byKind("object")
    • claimed → representable
    • exact bodies for BinderIgnitionSection and MaxDensitySection
    • blank-optional omission, and required-blank → not ready
    • de_DE comma cells
    • row vectors inside the object
    • the prefill round trip, in C and de_DE
    • the unclaimed case staying unrepresentable
  • 3 new tests/test_nested_forms.cpp cases: a required nested member's FieldMeta, and a depth-2 optional member in both the $ref and the inline anyOf form.

Mutation checks (actually run):

Break Red
Leaf encoder in encodeObjectValue replaced by plain string quoting 9 of 24 QML cases (all in the new file; 410/419 suite-wide)
forms.hpp optional recursion reverted 2 of the 3 new C++ cases (9 assertions)

Local results: MSVC 14.5x, Qt 6.11.1, QT_QPA_PLATFORM=offscreen:

  • forms_qml_logic 419 passed, 0 failed
  • ctest -R forms 10/10
  • full ctest 1596/1596

Out of scope

  • An object or collection inside a row of a claimed std::vector<Row> is still unrepresentable, because row cells are texts.
  • Nested cells use the canonical unit only. There is no unit selector per cell.

🤖 Generated with Claude Code

… encodes it member by member

A struct member -- `IgnitionSpecimen specimen`, or
`std::optional<PycnometerDetermination> determination1` -- has no built-in
control: DynamicForm flattens it to one text field, reports it
unrepresentable, and a form that must carry it never becomes ready. #809
gave a host slot a way out for `std::vector<Row>`; a nested object had none.

When a registered slot claims a top-level object member:

  - `fields` marks it `isObject` and describes its members as
    `objectFields` with the same descriptor builder (label, unit, decimals,
    readOnly, required, kind flags). The description recurses: a member that
    is itself an object carries its own `objectFields`, a collection its
    `itemFields`. It is bounded by `maxObjectDepth` (4) and stops at the
    first repetition of a `$defs` type on the path, so a self-referential
    type cannot loop.
  - `unrepresentable` is "" for it and for every object or collection
    described inside it.
  - The slot writes `{member: cellText | nestedValue}` objects through the
    new optional `setObject(value)` and reads them back through the
    `objectValue` binding. `encodeObjectValue` encodes each leaf with
    `encodeFieldText` (locale, precision, bounds), a nested object
    recursively and a collection with the row encoder. A blank optional
    leaf or object is omitted; a blank required leaf or object keeps the
    form unready.
  - `prefill` / `prefillFromJson` decode a stored object back into that
    shape (`decodeObjectValue`), so prefill -> submit round-trips.

An object member no slot claims behaves as before;
tst_DynamicFormNestedAggregate.qml is untouched and green.

Schema side: `recurseIntoNestedAggregateIfAny` did not see through
`std::optional<Sub>`, which glaze spells as an `anyOf` over Sub's schema and
null, so an optional sub-record's members had no x-order, title, required
or FieldMeta (unit, x-displayDecimals, label). It now recurses into the
non-null branch. Three new test_nested_forms.cpp cases pin it; without the
fix two of them fail.

The new tst_DynamicFormObjectSlot.qml (24 cases) asserts submitted bodies
for both consumer shapes; replacing the leaf encoder with plain string
quoting reddens 9 of them. forms_qml_logic: 419 passed, full ctest 1596/1596
on MSVC / Qt 6.11.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Yaraslaut
Yaraslaut merged commit 6df7e72 into master Sep 25, 2026
26 of 28 checks passed
@codecov

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/forms/forms.hpp 83.33% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

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.

1 participant