Repository navigation
feat: repeating table rows (#349 phase 3) - #531
Merged
Merged
Conversation
repeatOf, isRepeating, setRepeat, templateRow and repeatRowName for table.repeat, and withKeys leaves a repeating table's rows unkeyed. PENDING in i18n-fallbacks lists keys the languages package hasn't shipped yet. Refs #349
A characterization snapshot before the matrix row builder is factored out for repeating rows. Refs #349
A repeating table renders min rows from its template with positional names, numbered row headers, a remove button per row, an Add button and a status region. matrixRowConfig builds one row, so added rows match the first render; dom.tableOptions shares the hook's options. Refs #349
One delegated listener per form adds rows from the template and removes them, renumbering later rows so keys stay contiguous. Focus moves to the new row or the next remove button, a status region announces the change, and formeo:rowschange reaches onChange. Refs #349
The userData setter grows a repeating table to the rows its keys name (never past max, or 500 rows when there is no max), and new rows adopt condition-hidden and skipped-page state. userFormData labels positional rows, and row and cell addresses on a repeating table resolve to nothing. Refs #349
The userData setter synced the button limits once per added row, a whole-table query each time. It now syncs once after growing. Adds a test that setter growth fires no rowschange, no onChange and no announcement. Refs #349
Input group code moves to renderer/input-groups.js. The Add label is translated, the remove button is named and works when its icon is clicked, focus and a status region follow add and remove, and formeo:rowschange fires. Copies of radio, checkbox and multiple select groups and matrices are named <name>-<n> and renumbered; copies are cached so userFormData labels them; a field with plain DOM children no longer throws. A repeating table inside a copy renders but neither clicks nor the userData setter add or remove its rows. Refs #349
The userData setter fills same-named text inputs, textareas and selects in order, and creates the input group copies that array answers and <name>-<n> keys need (up to 500), so saved answers round-trip into a fresh render. Setter lookups match names only, since a copy's <id>-<n> name collided with its option input ids. Refs #349
Copies the userData setter adds inside a skipped page or a condition-hidden group are disabled or not required like their neighbours. adoptRow moves to row-actions.js as adoptInto, shared by repeating table rows and input group copies. Refs #349
table.repeat { min, max } in the formData schema and TableData, plus RowsChangeDetail for
formeo:rowschange, which onChange now also receives.
Refs #349
A Repeating rows checkbox (locked, with a hint, until the table has an input column), and Minimum and Maximum rows inputs normalised on commit. While repeating, the grid shows only the template row, whose Required checkbox applies to every row. Refs #349
Typing keeps the items a match sits under and the items under it, so a row name no longer hides its cells. Right and Left move into and out of nested rows, cells and options, which keyboard users could not reach. Repeating tables list no rows or cells. Refs #349
Visually hidden helpers, 24px remove targets with a focus outline, the stacked actions cell, the panel's row limits, and nested picker lists that stay open around the keyboard's active item. Refs #349
How to turn on repeating rows, the template row, limits, positional names and userData, the setter, keyboard use and formeo:rowschange; and input group names, restore and accessibility. Refs #349
Panel keyboard setup, keyboard add and remove with focus and announcements, a userData round trip into a fresh render, required rows, stacking at 360px, input group names and restore, and reaching matrix cells in the condition picker from the keyboard. The shared table e2e helpers move to tests/helpers/table.js. Refs #349
Assert the input group restore recreates clones, build the table from the keyboard including column types and limits, check each stacked card ends with its remove button, and wait for focus before typing into a new row. Refs #349
The setter stopped accepting element ids when it began matching names only, so
`{ 'f-t1': 'x' }` no longer filled an unnamed text field whose name came from
its label. Names are still tried first; an id now fills a single text-like
control or select as a last resort, never a checkbox or radio, whose option ids
look like an input group copy's `<base>-<n>` name.
A name lookup that mixes id and name matches now returns the one named control
itself, so a multiple select found that way takes every value of an array.
Refs #349
An input group answer array longer than the 501 inputs the setter may create lost its extra values silently, though the docs say they're reported. They now join the setter's warning for keys with no field, e.g. `email (values 502-600)`. Refs #349
A repeating table inside an input group copy doesn't repeat, yet it showed Add and remove buttons that did nothing, and bringing a skipped page back re-enabled them. A copy now renders those buttons hidden and disabled, and repeatingTables leaves copy tables out, so a skipped page coming back doesn't re-sync them. The remove button's display rule beat [hidden], so it gets its own [hidden] rule. Refs #349
A repeating table imported with `rows: []` showed a blank template row in the Table panel that wasn't in its data, so cell edits and the Every row required toggle changed nothing. The panel now edits from a table holding that template row, and turning repeating on for a table with no rows stores it too. Refs #349
onChange's formeo:rowschange event targets the table or group wrapper, which has no name, so the target.name recipe needs an event.type check first; the renderer docs and the onChange type comment now say so. Re-wraps the input group Names and Accessibility paragraphs and a table docs line to 120 columns. Refs #349
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Clone cache lifecycle, unbounded row allocation, and incomplete event typings remain unresolved.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Adds repeatable table rows to the renderer/editor, while improving input-group restoration, accessibility, condition selection, schema, and public types.
Changes:
- Implements repeatable table rows with limits, positional names, restoration, events, and accessibility.
- Refactors input-group cloning and condition-picker navigation.
- Adds extensive unit, type, CSS, and Playwright coverage plus documentation.
| File | Description |
|---|---|
tools/formdata-schema.mjs |
Adds repeating-table schema. |
tests/table.spec.js |
Uses shared table helpers. |
tests/table-repeat.spec.js |
Adds repeat-row E2E coverage. |
tests/table-matrix.spec.js |
Uses shared table helpers. |
tests/helpers/table.js |
Provides shared Playwright utilities. |
src/types/formeo.test-d.ts |
Tests new public types. |
src/types/formeo.d.ts |
Adds repeat and event-detail types. |
src/lib/sass/css-properties.test.mjs |
Tests new CSS rules. |
src/lib/sass/components/_field-edit.scss |
Styles repeat-limit controls. |
src/lib/sass/components/_autocomplete.scss |
Keeps keyboard-selected branches visible. |
src/lib/sass/base/_bs.scss |
Styles repeating-table controls. |
src/lib/sass/_render.scss |
Improves input-group remove buttons. |
src/lib/sass/__fixtures__/formeo-baseline.css |
Updates compiled CSS fixture. |
src/lib/js/renderer/table-repeat.test.js |
Tests repeating-row behavior. |
src/lib/js/renderer/table-matrix.test.js |
Updates clone naming expectations. |
src/lib/js/renderer/row-actions.js |
Shares row/group actions. |
src/lib/js/renderer/repeat-rows.js |
Implements runtime row repetition. |
src/lib/js/renderer/renderer.test.js |
Tests identifier-based restoration. |
src/lib/js/renderer/input-groups.test.js |
Tests input-group cloning and restore. |
src/lib/js/renderer/input-groups.js |
Implements deterministic group copies. |
src/lib/js/renderer/index.js |
Integrates repetition and restoration. |
src/lib/js/renderer/helpers.js |
Exports shared skip helpers. |
src/lib/js/i18n-fallbacks.test.mjs |
Tracks pending translations. |
src/lib/js/formdata-schema.test.mjs |
Tests repeat schema generation. |
src/lib/js/components/fields/table-panel.test.js |
Tests repeat editor controls. |
src/lib/js/components/fields/table-panel.js |
Adds repeat configuration UI. |
src/lib/js/components/autocomplete/table-targets.test.mjs |
Tests filtering and keyboard navigation. |
src/lib/js/components/autocomplete/helpers.mjs |
Adds hierarchical filtering. |
src/lib/js/components/autocomplete/autocomplete.mjs |
Adds nested keyboard navigation. |
src/lib/js/common/table.test.mjs |
Tests repeat utilities. |
src/lib/js/common/table.mjs |
Defines repeat data and markup. |
src/lib/js/common/table-text.test.mjs |
Tests new table strings. |
src/lib/js/common/table-text.mjs |
Adds table translations. |
src/lib/js/common/table-repeat-dom.test.js |
Tests rendered repeat markup. |
src/lib/js/common/input-group-text.test.mjs |
Tests group translations. |
src/lib/js/common/input-group-text.mjs |
Adds group translation fallbacks. |
src/lib/js/common/dom.js |
Centralizes table rendering options. |
src/lib/js/common/__snapshots__/table-repeat-dom.test.js.snapshot |
Records compatibility markup. |
docs/typescript.md |
Documents new exported types. |
docs/renderer/renderer.md |
Documents events and input groups. |
docs/controls/table.md |
Documents repeating rows. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
2 tasks done
repeatOf capped nothing, so a huge repeat.min built that many rows, or threw a RangeError past the maximum array length, before the form rendered. min is now capped at REPEAT_MIN_LIMIT (500) in repeatOf, the schema, the panel's Min input and the docs. Refs #349
Copies are cached in renderer.components, which only destroy() reset. Rendering again kept the last render's copies, so labels and conditions could read stale entries and the cache grew with every render. getRenderedForm now starts from an empty cache. Refs #349
Collaborator
Author
|
🎉 This PR is included in version 5.19.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
table.repeat = { min, max }, the person filling in the form adds and removes rows.rows[0]is the template: its static text and Required apply to every row.order[0][qty], radiosorder[0]) and are renumbered after a remove. With row headers on, rows are numbered from the template label ("Item 2").minand + Row atmax.userDatasetter recreates the rows a saved answer needs, up tomax(or 500 with no max).userFormDatalabels positional rows ("Order: Item 2, Qty").<name>-<n>, renumbered after a remove)userFormDatalabels their answersformeo:rowschange, a bubblingCustomEventwith{ action: 'add' | 'remove', index }, is dispatched by tables and input groups, and the renderer callsonChangefor it.repeat,TableRepeat/RowsChangeDetailtypes, CSS (24px remove targets with a focus outline, stacked cards end with the remove button), and docs indocs/controls/table.mdanddocs/renderer/renderer.md.Compatibility
<name>-<n>instead of a random id, so their answers can be restored.add-input-group/remove-input-groupclasses are unchanged.onChangealso fires when a row or group is added or removed. For those eventstargetis the table or group wrapper, which has noname, so checkevent.type === 'formeo:rowschange'before readingtarget.name.userDatasetter:f-x-0) no longer works. Unnamed checkables are namedf-<id>, so that key still does.xshouldn't also contain a control namedx-<n>.@draggable/formeo-languagesships them (branchfeat/table-repeat-keys). They're listed inPENDINGini18n-fallbacks.test.mjs.Follow-up
@draggable/formeo-languagesand emptyPENDING.Test plan
npm test(1175),npm run test:types,npm run lint,npx biome check src tests,npm run build,npm run test:distPlaywright:
tests/table-repeat.spec.jscovers:The full suite passed: 227 passed, 3 skipped.
The
dist/formData_schema.jsondiff is onlyrepeatand the table description.Phase 3 (repeating rows) is ready for review on feat/349-table-repeat:
table.repeat{min, max} with a template row, positional keys, setter-created rows, input-group names, labels, restore and accessibility, and picker filter and keyboard fixes. Languages: feat/table-repeat-keys.Refs #349
Please squash-merge.