fix: stop rows and columns rendering their editor label - #484
Conversation
A row and a column carry a config.label of "row" and "column" that the editor puts there for the stage. The renderer passed it straight to dom.create, which renders a label whenever one is set and hideLabel is not, so every rendered form grew a stray <label>row</label> above each row and <label>column</label> above each column.
Try it out at https://draggable.github.io/formeo/ where I simply add rows or columns and then click the “Render form” button. But if the row is created by adding an element, that element doesn't have it. If you start with a row and add an element to it, the label remains.
|
I think I am missing something. Screencast.From.2026-08-08.15-51-49.mp4 |
I really don't get it. I guess the only difference is dark mode. https://github.com/user-attachments/assets/3b4f64e5-b9e9-4f2e-82bf-9d6d5bcae635 |
| * @param {Object} config | ||
| * @return {Object} config that renders no label of its own | ||
| */ | ||
| const layoutConfig = (config = {}) => ({ ...config, hideLabel: true }) |
There was a problem hiding this comment.
Would this override the label for input groups? There are cases where we want to row and column to have a label but if this default to hide then they may not appear. Say for example you have an input group for contacts, without the label it might not be clear. The editor could add another row just for the label but then would lose the fieldset capability.
can you try clearing the page storage? maybe something carried over from a previous config? I'm not able to replicate this bug. This is unique for sure. edit: just saw your videos. are you on safari? it may be safari related. it should still be fixed, just need to pin down what safari is doing differently. |
|
Claude identified it D&D vs. click VerdictThe fix works (the symptom disappears), but it treats the effect, not the cause. The reproduction description matches the actual root cause exactly — and that cause lives in the editor, not the renderer. Where the problem actually originatesThe The drag & drop handler copies it into the data of the created component — const { controlData: { meta: { id: metaId }, ...elementData } } = Controls.get(item.id)
set(elementData, 'config.controlId', metaId)
...
const component = action(elementData, newIndex) // ← elementData = { config: { label: 'row', controlId } }That explains all three branches of the report:
Extra evidence that this is a bug: clicking a control (instead of dragging) goes through What the renderer fix does not solve
RecommendationKeep the renderer guard (it is defensive and repairs already-saved forms that contain the bad data), but add a fix for the root cause in const isLayout = metaId.startsWith('layout-')
const controlType = isLayout ? metaId.replace(/^layout-/, '') : 'field'
// layout controls carry only the panel button caption, no component data
const componentData = isLayout ? {} : elementData
...
const component = action(componentData, newIndex)This aligns the drag path with the click path and eliminates points 1–3. Worth adding an editor-level test asserting that a dropped row/column has no Minor note on the fix itself: |
A layout control's config.label is the caption of its button in the controls panel — "row", "column". The drag-drop handler passed the whole control payload to the component it created, so a dropped row or column stored that caption as its own label: the renderer printed <label>row</label> above every row, and the condition builder offered "row" as the component's name. A dropped row also lost its default config, because the control's config replaced it wholesale. Layout controls now contribute no data to the row or column they create, which starts from its own defaults — the same result the click path already produced. bad label are cleaned up on the consumer side.
kevinchappell
left a comment
There was a problem hiding this comment.
good find and fix.
|
🎉 This PR is included in version 5.1.3 🎉 The release is available on: Your semantic-release bot 📦🚀 |





What
Dragging a row or column control onto the stage stores the caption of the control's button as the layout container's own label. The rendered form then prints it:
Every such row renders
<label>row</label>and every column<label>column</label>, visible to whoever fills the form in.Why it happens
config.labelon a layout control is the caption of its button in the controls panel (Control.domrenders it as.control-label), not data for the component the control creates. The drop handler inComponent.onAddstrips onlymetaoff the control payload and hands the rest toaddChild:So the caption lands in the row's/column's stored
config, anddom.createrenders a label wheneverconfig.labelis set andconfig.hideLabelis not.That is exactly how it reproduces:
label: 'row'❌label: 'column'❌Clicking a layout control instead of dragging it already produced clean data —
Controls.addElementdiscards the payload for the layout group — so the two paths disagreed with each other.Two further consequences beyond the stray label:
fieldset,legend,inputGroup): the control'sconfigreplaces the defaults wholesale in the shallow spread theRowconstructor doesconfig.labelbeforeconfig.legend, so such a row is offered as "row" when picking a condition targetFix
Layout controls now contribute no data to the row or column they create — the container starts from its own defaults, the same result the click path already produced. Field controls keep passing their data through unchanged.
The renderer is deliberately left alone. Suppressing the label at render time would have hidden the symptom while every newly built form kept storing it, and would have overridden
hideLabelfor layout containers for good. Forms already saved with the bad data need a one-off cleanup ofconfig.labelon their rows and columns.Tests
src/lib/js/components/layout-controls.test.jsdrives the drop handler the way Sortable does:config.labelandconfig.controlIdThe first two fail without the change.