Skip to content

forms: x-blankAs "empty" -- a string cleared in an edit form submits "" instead of being omitted - #824

Merged
Yaraslaut merged 1 commit into
masterfrom
feature/forms-blank-as-empty
Sep 25, 2026
Merged

Yaraslaut merged 1 commit into
masterfrom
feature/forms-blank-as-empty

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

What / why

DynamicForm leaves a blank control out of the payload. Consider an edit form prefilled from a stored record, where the model reads an absent std::optional<std::string> as "leave unchanged" and "" as "clear". In that form a prefilled string cannot be cleared: when the user deletes the text, the member is omitted and the stored value stays.

Contract

Schema key: property-level "x-blankAs": "empty", read with the same dual-read as the other property keys. The descriptor exposes it as blankAsEmpty. It is only read for fields of kind string, including ["string","null"] and the nullable anyOf spelling.

What a blank control submits, by field state since the last prefill / resetFields:

Field state Blank control submits
Prefilled with a string, "" included "field": ""
Non-blank at any point via any write path (control, setFieldValue, slot setValue), then cleared "field": ""
Never prefilled and never non-blank. A stored null does not count as prefilled. omitted, as before

Other rules:

  • Required fields: unchanged. A required field left blank is still unfilled, so the form is not ready.
  • Non-string kinds (number, enum, Choice, datetime, …) ignore the key.
  • C++: FieldMeta gains BlankAs blankAs{BlankAs::Omit} (enum class BlankAs { Omit, Empty }) plus withBlankAs(). It emits "x-blankAs": "empty", but only on a std::string / std::optional<std::string> member.
  • Wire: unchanged. The key only affects presentation.

The engagement rule has one consequence to be aware of. In a create form, typing into such a field and then clearing it also submits "": the user did touch the field. Only a field that was never touched is omitted.

Tests

  • New src/qt/forms/tests/tst_DynamicFormBlankAs.qml, 12 cases, all asserting the submitted body. It covers:
    • a prefilled field that is cleared submits ""
    • an untouched field is omitted
    • a field typed into and then cleared submits ""
    • a stored "" round-trips; a stored null does not engage the field
    • a field without the key is still omitted
    • a non-string field ignores the key
    • a required field is still gated
    • both reset and re-prefill disengage the field
    • a slot's setValue("") behaves the same
  • New tests/test_forms_blank_as.cpp checks that the key is emitted on string members, and not by default or on a double, and that the wire is unchanged.

Mutation checks (actually run):

Break Red
A cleared field never submits "" 4 of 12
Engagement ignored, so an untouched field also submits "" 6 of 12

Local results (MSVC / Qt 6.11.1, offscreen):

  • forms_qml_logic: 409 passed, 0 failed
  • morph_tests "[forms]": all passed

Independent of #822 / #823.

🤖 Generated with Claude Code

…"" instead of being omitted

DynamicForm omits a blank control from the payload. For an edit form
prefilled from a stored record whose model reads an absent
std::optional<std::string> as "leave unchanged" and "" as "clear", that
made a prefilled string impossible to clear: deleting the text sent
nothing, and the stored value survived.

A string property carrying "x-blankAs": "empty" now submits "" when its
control is blank -- once the field is engaged since the last prefill or
reset: prefilled with a string ("" included), or non-blank at any
revalidate() since, whichever path wrote it (a control, setFieldValue, a
slot's setValue). A field never prefilled and never typed into is still
omitted, and a stored null does not engage it. `required` is unchanged: a
required field left blank is still unfilled. The key is read only for kind
"string" (nullable spellings included); every other kind ignores it.

C++: FieldMeta gains `BlankAs blankAs{BlankAs::Omit}` (+ withBlankAs()),
emitted as "x-blankAs": "empty" on a std::string /
std::optional<std::string> member only. Presentation only; the wire is
unchanged.

tst_DynamicFormBlankAs.qml (12 cases) asserts submitted bodies: never
emitting "" reddens 4, ignoring engagement reddens 6.
test_forms_blank_as.cpp pins the emission. forms_qml_logic: 409 passed,
[forms] C++ cases all green on MSVC / Qt 6.11.1.

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

codecov Bot commented Sep 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 42.85714% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/forms/forms.hpp 42.85% 4 Missing ⚠️

📢 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