Skip to content

feat(controls): control sets, a control that adds a row of fields at once - #511

Merged
kevinchappell merged 9 commits into
mainfrom
feat/227-control-sets
Sep 28, 2026
Merged

kevinchappell merged 9 commits into
mainfrom
feat/227-control-sets

Conversation

@kevinchappell

Copy link
Copy Markdown
Collaborator

What

Control sets: a control that adds several predefined fields at once, like formBuilder's inputSets. Clicking or dropping one adds a single new row holding all of its fields, with new ids every time.

A set is an ordinary controls.elements entry with a controlSet key:

{
  id: 'address-set-control', // optional: lets your code call controls.addElement('address-set-control')
  meta: { group: 'common', id: 'address-set', icon: 'rows' },
  config: { label: 'Address' },
  controlSet: {
    layout: 'stacked', // default: one column; 'columns' gives each field its own column
    row: { config: { fieldset: true, legend: 'Address' } }, // optional data for the new row
    fields: [
      { control: 'text-input', attrs: { name: 'street' }, config: { label: 'Street' } },
      { control: 'select', attrs: { name: 'country' }, options: [{ label: 'Canada', value: 'ca', selected: false }] },
    ],
  },
}
  • Members with control start from that control's data (its meta.id) and override it; arrays such as options replace the control's instead of being appended. Members without control are field data as is, and can name their control with meta.id.
  • Placement: a click appends the row to the current page. A drop on a page puts it at the drop position; a drop on a row or column puts it in a new row right after that row (a set never goes inside an existing column, and the row it was dropped on keeps its column widths).
  • Hooks: onBeforeAdd / formeoBeforeAdd (Feature: event/action upgrades #281) run once for the whole set with componentType: 'controlSet', the set's controlId, data: { layout, row, fields }, and the page and index the row goes to. A held drop is added once allowed, unless its target was removed meanwhile. The usual onAddRow / onAddColumn / onAddField events follow. controls.addElement(id) adds a set without the hook.
  • Errors: an unknown member control, a layout control, another set, or a member that isn't an object is skipped with a formeo: control set "<id>" … console warning. A set left with no fields adds nothing and runs no hook.
  • Dragging a set shows its own button, not a single-field ghost preview.
  • The demo has an Address set (street, city, postcode, country).

Existing controls and existing onBeforeAdd handlers are unchanged; handlers that switch on componentType may now also see 'controlSet'.

Docs

docs/controls/custom-controls.md ("Control sets"), a pointer in docs/options/controls/README.md, and 'controlSet' in the onBeforeAdd section of docs/options/events/README.md.

Testing

  • npm test: 701 pass (672 on main). New: src/lib/js/components/controls/control-set.test.js, src/lib/js/components/control-sets.test.js
  • npm run lint: clean
  • Playwright: 174 passed, 3 skipped (the pre-existing test.skips). New spec: tests/control-sets.spec.js

Follow-ups

  • Save a canvas row as a set; multi-row and nested sets; inserting a set into an existing column
  • Show/hide a set's row as one unit (Visability by Group #277)
  • controls.addElement could accept a control's meta.id (today it needs the control's own id, for every control)

Fixes #227

A control with controlSet.fields describes one row of fields. Each member can name a registered
control and override its data (arrays such as options are replaced), or give field data as it is.
Every expansion is a fresh copy; unknown controls are skipped with a warning.

Refs #227
…s clicked

Clicking a control set runs onBeforeAdd once with componentType 'controlSet'
and the expanded { layout, row, fields }, then adds one new row holding every
field (one column, or one column per field with layout 'columns').
controls.addElement adds a set too. A set's drag keeps its button instead of
a ghost preview.

Refs #227
A dropped control set becomes one new row: at the drop index on a stage, or right after the row
it was dropped in. onBeforeAdd reports that stage and index. A held drop is added once allowed,
unless its target was removed meanwhile; a set with no fields adds nothing.

Also fixes Stage#onAdd swallowing the return value of super.onAdd, which the new
"onAdd returns the new row" drop test exposed for a stage-level drop.

Refs #227
How to define a set in controls.elements, how members start from a control and override it, where
a clicked or dropped set goes, and what onBeforeAdd receives for one.

Refs #227
…ed on it

A control set dropped on a row never changes that row's columns (it lands
in a new row right after it), so Row#onAdd should not call
autoColumnWidths for that drop. It previously always did, silently
resetting any custom widths (e.g. 25%/75%) to equal shares and firing
columnResized. Same fix covers an empty-set drop on a row.

Refs #227
expandControlSet destructured each controlSet.fields member directly,
throwing a TypeError on a null (or otherwise non-object) member. Since
describeControl runs before dom.remove(item) in Component#onAdd, a throw
here left the dragged control's placeholder stuck on the canvas. Skip a
non-object member with a warning, in the same format as an unknown
control's.

Refs #227
…eral

member names its control

The control sets doc left `controls.addElement(id)`'s `id` ambiguous with
`meta.id`; only a top-level `id` on the definition works, and it must be
distinct from `meta.id` or a generated uuid is used. Also document that a
literal (control-less) member can still name its control via `meta.id`,
which becomes its `config.controlId`. Rewrap the 134-column pointer line
in docs/options/controls/README.md's "## elements" section to fit 120.

Refs #227
…nning

The drag e2e test only checked that the Address set was added, not that it
landed where it was dropped; assert its row's position in the stage's own
children order instead. The veto test waited a fixed 300ms without
confirming the document listener actually ran; have it set a flag and poll
for it before asserting no rows were added.

Refs #227
Copilot AI lite review requested due to automatic review settings September 28, 2026 14:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@kevinchappell
kevinchappell merged commit c0d88c3 into main Sep 28, 2026
2 checks passed
@kevinchappell
kevinchappell deleted the feat/227-control-sets branch September 28, 2026 18:59
@kevinchappell

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 5.12.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to create a control in Formeo like inputSets in formbuilder,

2 participants