Skip to content

fix(pages): stop wrapping layout placeholders in a synthetic container (#760) - #76

Merged
ako merged 2 commits into
mainfrom
claude/layout-placeholder-wrapper-760
Aug 1, 2026
Merged

fix(pages): stop wrapping layout placeholders in a synthetic container (#760)#76
ako merged 2 commits into
mainfrom
claude/layout-placeholder-wrapper-760

Conversation

@ako

@ako ako commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Fixes mendixlabs#760.

The bug

Every mxcli-authored page carried a container nobody asked for. The builder wrapped each non-empty layout placeholder in a Forms$DivContainer named conditionalVisibilityWidget<N>, so creating a single button produced a button and a container — visible in the Studio Pro widget tree of every page mxcli wrote.

Reproduced exactly as reported:

DivContainer count: 1
wrapper names: ['conditionalVisibilityWidget1']
ActionButton count: 1

Root cause — an artificial model constraint, not a BSON requirement

Forms$FormCallArgument carries a Widgets array, and both writers already emitted an array containing exactly one element. The wrapper existed only because pages.LayoutCallArgument declared a single Widget field, so N widgets had to be squeezed through a 1-widget hole.

Verified against Mendix's own output rather than inferred. Administration.Account_Overview in a mx create-project 11.12.2 app:

placeholder 'Atlas_Core.Atlas_Default.Main': 2 top-level widget(s)
  -> ['Forms$DivContainer', 'Forms$LayoutGrid']
conditionalVisibilityWidget occurrences: 0

Two top-level widgets sitting directly in the placeholder, no wrapper anywhere.

That also corrects a comment of ours claiming the wrapper is what "mxcli (and Studio Pro) adds as a layout placeholder container". Studio Pro uses that name when wrapping a widget for conditional visibility, not for placeholders — the same reasoned-by-analogy error that produced mendixlabs#812.

Corroborating signal

Three places already worked around the container: DESCRIBE unwrapped it to avoid "a phantom CONTAINER wrapping all widgets", and the catalog builder skipped it as "transparent". When three readers are compensating for a construct, it probably shouldn't be written.

Those readers are deliberately kept — projects authored before this fix still contain wrappers and must keep reading correctly. This changes what we write; it does not retro-clean existing pages.

The change

LayoutCallArgument.WidgetWidgets []Widget, and the builder places the authored widgets directly. Seven call sites across the two writers, the MCP backend, the builder and one example.

Verification

End-to-end on a real project, identical on both engines:

input placeholder contents wrappers
one button [Forms$ActionButton] 0
three widgets [Forms$DynamicText, Forms$ActionButton, Forms$DivContainer] 0

The DivContainer in the second row is the one the author wrote — the fix removes the synthetic wrapper, not real containers.

mx check   ->  "The app contains: 0 errors."
DESCRIBE   ->  round-trips to the authored MDL

Mutation-checked: truncating the writer's widget list fails the new test with the reported symptom. Full suite green; corpus 237/39 including the new repro fixture.

Note on test placement

I put the regression test at the writer level (sdk/mpr) rather than the builder. The builder test needs layout resolution through a backend, and the harness plumbing was costing more than the coverage was worth; the writer test needs no backend and still pins the model change and both serializers. The builder behaviour is covered by the end-to-end BSON evidence above.

🤖 Generated with Claude Code

https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA


Generated by Claude Code

claude added 2 commits August 1, 2026 10:10
mendixlabs#760)

Every mxcli-authored page carried a container nobody asked for. The builder
wrapped each non-empty layout placeholder in a Forms$DivContainer named
"conditionalVisibilityWidget<N>", so creating a single button produced a button
AND a container, visible in the Studio Pro widget tree of every page mxcli wrote.

The wrapper was never a BSON requirement. Forms$FormCallArgument carries a
`Widgets` array and a Studio Pro page fills it with its top-level widgets
directly — both writers already emitted an array containing exactly one element.
It existed only because pages.LayoutCallArgument declared a single `Widget`
field, so N widgets had to be squeezed through a 1-widget hole.

Verified against Mendix's own output rather than inferred:
Administration.Account_Overview in a `mx create-project` 11.12.2 app has two
top-level widgets in one placeholder (a DivContainer and a LayoutGrid) and zero
conditionalVisibilityWidget wrappers anywhere.

That also corrects a comment of ours which claimed the wrapper is what "mxcli
(and Studio Pro) adds as a layout placeholder container". Studio Pro uses that
name when wrapping a widget for conditional visibility, not for placeholders —
the same reasoned-by-analogy error that produced mendixlabs#812.

Three places already worked around the container: DESCRIBE unwrapped it to avoid
"a phantom CONTAINER wrapping all widgets", and the catalog builder skipped it as
"transparent". Those readers are deliberately KEPT — projects authored before
this fix still contain wrappers, and must keep reading correctly. This changes
what we write, it does not retro-clean existing pages.

LayoutCallArgument.Widget becomes Widgets []Widget; the builder places the
authored widgets directly. Seven call sites across the two writers, the MCP
backend, the builder and one example.

Verified end-to-end on a real project, identical on both engines:

  one button      -> placeholder widgets [Forms$ActionButton]          0 wrappers
  three widgets   -> [Forms$DynamicText, Forms$ActionButton, Forms$DivContainer]
                     (the DivContainer is the one the author wrote)    0 wrappers
  mx check        -> "The app contains: 0 errors."
  DESCRIBE        -> round-trips to the authored MDL

Reverting the writer's widget list fails the new test with the reported symptom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012XR649rKk68z6gBpngu6MA
…lder-wrapper-760

# Conflicts:
#	.claude/skills/fix-issue.md
@ako
ako merged commit 96b320c into main Aug 1, 2026
3 checks passed
ako pushed a commit that referenced this pull request Aug 1, 2026
…driver

Every bug fix appends a row to the symptom table in .claude/skills/fix-issue.md,
so two concurrent fixes always collide on the same line — five resolution rounds
in one week. Moving the insertion point from the top of the table to the bottom
did not help: both sides still append to the same place, so the collision simply
moved with it (PRs #76, #77 and #78 all hit it).

git's built-in "union" merge driver keeps BOTH sides of a conflicting hunk
instead of raising a conflict. That is exactly right here: the file is only ever
appended to, and it is looked up by matching a symptom rather than read in order,
so row order carries no meaning.

Verified: two branches that each append a row now merge with no conflict and
both rows present.

Caveat: union applies to the whole file, so two branches editing the same *prose*
line would silently keep both instead of conflicting. The failure is a visible
duplicated line, not corruption. Splitting the table into its own file — so union
covers only append-only content — is the follow-up, deferred because that
restructure would conflict with the three open PRs that all touch this table.
ako pushed a commit that referenced this pull request Aug 1, 2026
The rationale in fix-issue.md and CLAUDE.md claimed that appending at the end of
the table avoids the conflict two concurrent fixes cause. It does not — both
sides still append to the same line, which is why #76, #77 and #78 each hit it
again after that convention was adopted.

Merging is handled by the merge=union driver in .gitattributes (added in #77).
Appending at the end is kept for readable diffs and rough chronology, which is a
human reason, not a git one.
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.

Unnecessary container creation called ConditionalVisibilityWidget

2 participants